mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-24 17:30:03 +00:00
Issue improvements + Process Support (#2281)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
ec0795e687
commit
d761662c21
@ -51,7 +51,7 @@ import workbench, { createNavigateAction } from '@hcengineering/model-workbench'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
import tags from '@hcengineering/tags'
|
||||
import tags, { TagElement } from '@hcengineering/tags'
|
||||
import task from '@hcengineering/task'
|
||||
import {
|
||||
Document,
|
||||
@ -61,6 +61,8 @@ import {
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
IssueStatusCategory,
|
||||
IssueTemplate,
|
||||
IssueTemplateChild,
|
||||
Project,
|
||||
ProjectStatus,
|
||||
Sprint,
|
||||
@ -254,6 +256,53 @@ export class TIssue extends TAttachedDoc implements Issue {
|
||||
declare childInfo: IssueChildInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@Model(tracker.class.IssueTemplate, core.class.Doc, DOMAIN_TRACKER)
|
||||
@UX(tracker.string.IssueTemplate, tracker.icon.Issue, tracker.string.IssueTemplate)
|
||||
export class TIssueTemplate extends TDoc implements IssueTemplate {
|
||||
@Prop(TypeString(), tracker.string.Title)
|
||||
@Index(IndexKind.FullText)
|
||||
title!: string
|
||||
|
||||
@Prop(TypeMarkup(), tracker.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: Markup
|
||||
|
||||
@Prop(TypeIssuePriority(), tracker.string.Priority)
|
||||
priority!: IssuePriority
|
||||
|
||||
@Prop(TypeRef(contact.class.Employee), tracker.string.Assignee)
|
||||
assignee!: Ref<Employee> | null
|
||||
|
||||
@Prop(TypeRef(tracker.class.Project), tracker.string.Project)
|
||||
project!: Ref<Project> | null
|
||||
|
||||
@Prop(ArrOf(TypeRef(tags.class.TagElement)), tracker.string.Labels)
|
||||
labels?: Ref<TagElement>[]
|
||||
|
||||
declare space: Ref<Team>
|
||||
|
||||
@Prop(TypeDate(true), tracker.string.DueDate)
|
||||
dueDate!: Timestamp | null
|
||||
|
||||
@Prop(TypeRef(tracker.class.Sprint), tracker.string.Sprint)
|
||||
sprint!: Ref<Sprint> | null
|
||||
|
||||
@Prop(TypeNumber(), tracker.string.Estimation)
|
||||
estimation!: number
|
||||
|
||||
@Prop(ArrOf(TypeRef(tracker.class.IssueTemplate)), tracker.string.IssueTemplate)
|
||||
children!: IssueTemplateChild[]
|
||||
|
||||
@Prop(Collection(chunter.class.Comment), tracker.string.Comments)
|
||||
comments!: number
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), tracker.string.Attachments)
|
||||
attachments!: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -389,6 +438,7 @@ export function createModel (builder: Builder): void {
|
||||
TTeam,
|
||||
TProject,
|
||||
TIssue,
|
||||
TIssueTemplate,
|
||||
TIssueStatus,
|
||||
TIssueStatusCategory,
|
||||
TTypeIssuePriority,
|
||||
@ -403,16 +453,20 @@ export function createModel (builder: Builder): void {
|
||||
attachTo: tracker.class.Issue,
|
||||
descriptor: tracker.viewlet.List,
|
||||
config: [
|
||||
{ key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } },
|
||||
{ key: '', presenter: tracker.component.IssuePresenter },
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.PriorityEditor,
|
||||
props: { type: 'priority', kind: 'list', size: 'small' }
|
||||
},
|
||||
{ key: '', presenter: tracker.component.IssuePresenter, props: { type: 'issue' } },
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.StatusEditor,
|
||||
props: { kind: 'list', size: 'small', justify: 'center' }
|
||||
},
|
||||
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true, fixed: 'left' } },
|
||||
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
|
||||
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
|
||||
{ key: '', presenter: tracker.component.GrowPresenter, props: {} },
|
||||
{ key: '', presenter: tracker.component.GrowPresenter, props: { type: 'grow' } },
|
||||
{ key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
|
||||
{
|
||||
key: '',
|
||||
@ -429,7 +483,35 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
key: '$lookup.assignee',
|
||||
presenter: tracker.component.AssigneePresenter,
|
||||
props: { defaultClass: contact.class.Employee, shouldShowLabel: false }
|
||||
props: { issueClass: tracker.class.Issue, defaultClass: contact.class.Employee, shouldShowLabel: false }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: tracker.class.IssueTemplate,
|
||||
descriptor: tracker.viewlet.List,
|
||||
config: [
|
||||
// { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } },
|
||||
{ key: '', presenter: tracker.component.IssueTemplatePresenter, props: { type: 'issue', shouldUseMargin: true } },
|
||||
{ key: '', presenter: tracker.component.GrowPresenter, props: { type: 'grow' } },
|
||||
// { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.ProjectEditor,
|
||||
props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.SprintEditor,
|
||||
props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false }
|
||||
},
|
||||
// { key: '', presenter: tracker.component.EstimationEditor, props: { kind: 'list', size: 'small' } },
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter, props: { fixed: 'right' } },
|
||||
{
|
||||
key: '$lookup.assignee',
|
||||
presenter: tracker.component.AssigneePresenter,
|
||||
props: { issueClass: tracker.class.IssueTemplate, defaultClass: contact.class.Employee, shouldShowLabel: false }
|
||||
}
|
||||
]
|
||||
})
|
||||
@ -533,11 +615,16 @@ export function createModel (builder: Builder): void {
|
||||
const boardId = 'board'
|
||||
const projectsId = 'projects'
|
||||
const sprintsId = 'sprints'
|
||||
const templatesId = 'templates'
|
||||
|
||||
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: tracker.component.IssuePresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: tracker.component.IssueTemplatePresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.PreviewPresenter, {
|
||||
presenter: tracker.component.IssuePreview
|
||||
})
|
||||
@ -671,6 +758,12 @@ export function createModel (builder: Builder): void {
|
||||
label: tracker.string.Sprints,
|
||||
icon: tracker.icon.Sprint,
|
||||
component: tracker.component.Sprints
|
||||
},
|
||||
{
|
||||
id: templatesId,
|
||||
label: tracker.string.IssueTemplates,
|
||||
icon: tracker.icon.Issues,
|
||||
component: tracker.component.IssueTemplates
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -846,6 +939,10 @@ export function createModel (builder: Builder): void {
|
||||
filters: ['status', 'priority', 'assignee', 'project', 'sprint', 'dueDate', 'modifiedOn']
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['priority', 'assignee', 'project', 'sprint', 'dueDate', 'modifiedOn']
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
presentation.class.ObjectSearchCategory,
|
||||
core.space.Model,
|
||||
|
@ -15,7 +15,7 @@
|
||||
//
|
||||
|
||||
import type { Doc, PropertyType } from './classes'
|
||||
import type { Position, PullArray } from './tx'
|
||||
import type { Position, PullArray, QueryUpdate } from './tx'
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -72,6 +72,45 @@ function $pull (document: Doc, keyval: Record<string, PropertyType>): void {
|
||||
}
|
||||
}
|
||||
|
||||
function matchArrayElement<T extends Doc> (docs: any[], query: Partial<T>): any[] {
|
||||
let result = [...docs]
|
||||
for (const key in query) {
|
||||
const value = (query as any)[key]
|
||||
|
||||
const tresult: any[] = []
|
||||
for (const object of result) {
|
||||
const val = object[key]
|
||||
if (val === value) {
|
||||
tresult.push(object)
|
||||
}
|
||||
}
|
||||
result = tresult
|
||||
if (tresult.length === 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function $update (document: Doc, keyval: Record<string, PropertyType>): void {
|
||||
const doc = document as any
|
||||
for (const key in keyval) {
|
||||
if (doc[key] === undefined) {
|
||||
doc[key] = []
|
||||
}
|
||||
const val = keyval[key]
|
||||
if (typeof val === 'object') {
|
||||
const arr = doc[key] as Array<any>
|
||||
const desc = val as QueryUpdate<PropertyType>
|
||||
for (const m of matchArrayElement(arr, desc.$query)) {
|
||||
for (const [k, v] of Object.entries(desc.$update)) {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function $move (document: Doc, keyval: Record<string, PropertyType>): void {
|
||||
const doc = document as any
|
||||
for (const key in keyval) {
|
||||
@ -114,6 +153,7 @@ function $inc (document: Doc, keyval: Record<string, number>): void {
|
||||
const operators: Record<string, _OperatorFunc> = {
|
||||
$push,
|
||||
$pull,
|
||||
$update,
|
||||
$move,
|
||||
$pushMixin,
|
||||
$inc
|
||||
|
@ -114,6 +114,14 @@ export interface Position<X extends PropertyType> {
|
||||
$position: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface QueryUpdate<X extends PropertyType> {
|
||||
$query: Partial<X>
|
||||
$update: Partial<X>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -136,6 +144,13 @@ export type ArrayAsElementPosition<T extends object> = {
|
||||
[P in keyof T]-?: T[P] extends Arr<infer X> ? X | Position<X> : never
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ArrayAsElementUpdate<T extends object> = {
|
||||
[P in keyof T]-?: T[P] extends Arr<infer X> ? X | QueryUpdate<X> : never
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -164,6 +179,13 @@ export interface PushOptions<T extends object> {
|
||||
$move?: Partial<OmitNever<ArrayMoveDescriptor<Required<T>>>>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface UpdateArrayOptions<T extends object> {
|
||||
$update?: Partial<OmitNever<ArrayAsElementUpdate<Required<T>>>>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -193,6 +215,7 @@ export interface SpaceUpdate {
|
||||
*/
|
||||
export type DocumentUpdate<T extends Doc> = Partial<Data<T>> &
|
||||
PushOptions<T> &
|
||||
UpdateArrayOptions<T> &
|
||||
PushMixinOptions<T> &
|
||||
IncOptions<T> &
|
||||
SpaceUpdate
|
||||
|
@ -104,7 +104,11 @@
|
||||
const person = objects[selection]
|
||||
|
||||
if (!multiSelect) {
|
||||
selected = person._id === selected ? undefined : person._id
|
||||
if (allowDeselect) {
|
||||
selected = person._id === selected ? undefined : person._id
|
||||
} else {
|
||||
selected = person._id
|
||||
}
|
||||
dispatch('close', selected !== undefined ? person : undefined)
|
||||
} else {
|
||||
checkSelected(person, objects)
|
||||
|
@ -66,6 +66,7 @@
|
||||
if (selected !== undefined) {
|
||||
value = selected._id
|
||||
dispatch('change', value)
|
||||
dispatch('space', selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,4 +55,5 @@
|
||||
on:change={(evt) => {
|
||||
space = evt.detail
|
||||
}}
|
||||
on:space
|
||||
/>
|
||||
|
@ -653,7 +653,6 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
|
||||
if (q.options?.lookup !== undefined && handleLookup) {
|
||||
await this.lookup(q._class, doc, q.options.lookup)
|
||||
}
|
||||
|
@ -14,11 +14,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Asset } from '@hcengineering/platform'
|
||||
import { AnySvelteComponent, Icon, LabelAndProps, tooltip } from '@hcengineering/ui'
|
||||
import { AnySvelteComponent, Icon, IconSize, LabelAndProps, tooltip } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let size: IconSize
|
||||
export let selected: boolean = false
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { IconSize, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import StyledTextEditor from './StyledTextEditor.svelte'
|
||||
@ -10,6 +10,7 @@
|
||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||
|
||||
export let showButtons = true
|
||||
export let buttonSize: IconSize = 'small'
|
||||
export let focus = false
|
||||
|
||||
let rawValue: string
|
||||
@ -50,6 +51,7 @@
|
||||
<StyledTextEditor
|
||||
{placeholder}
|
||||
{showButtons}
|
||||
{buttonSize}
|
||||
isScrollable={false}
|
||||
bind:content={rawValue}
|
||||
bind:this={textEditor}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import presentation, { MessageViewer } from '@hcengineering/presentation'
|
||||
import { ActionIcon, IconCheck, IconClose, IconEdit, Label, ShowMore } from '@hcengineering/ui'
|
||||
import { ActionIcon, IconCheck, IconClose, IconEdit, IconSize, Label, ShowMore } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import StyledTextEditor from './StyledTextEditor.svelte'
|
||||
@ -13,6 +13,7 @@
|
||||
export let emphasized: boolean = false
|
||||
export let alwaysEdit: boolean = false
|
||||
export let showButtons: boolean = true
|
||||
export let buttonSize: IconSize = 'small'
|
||||
export let hideExtraButtons: boolean = false
|
||||
export let maxHeight: 'max' | 'card' | string = 'max'
|
||||
export let previewLimit: number = 240
|
||||
@ -85,9 +86,9 @@
|
||||
<StyledTextEditor
|
||||
{placeholder}
|
||||
{showButtons}
|
||||
{buttonSize}
|
||||
{maxHeight}
|
||||
{focusable}
|
||||
useReferences
|
||||
bind:content={rawValue}
|
||||
bind:this={textEditor}
|
||||
on:focus={() => {
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
|
||||
import { Scroller, showPopup } from '@hcengineering/ui'
|
||||
import { IconSize, Scroller, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import EmojiPopup from './EmojiPopup.svelte'
|
||||
@ -43,11 +43,11 @@
|
||||
export let content: string = ''
|
||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||
export let showButtons: boolean = true
|
||||
export let buttonSize: IconSize = 'large'
|
||||
export let isScrollable: boolean = true
|
||||
export let focusable: boolean = false
|
||||
export let maxHeight: 'max' | 'card' | string = 'max'
|
||||
export let withoutTopBorder = false
|
||||
export let useReferences = false
|
||||
|
||||
let textEditor: TextEditor
|
||||
|
||||
@ -145,28 +145,28 @@
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder>
|
||||
<StyleButton
|
||||
icon={Bold}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('bold')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Bold }}
|
||||
on:click={getToggler(textEditor.toggleBold)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Italic}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('italic')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Italic }}
|
||||
on:click={getToggler(textEditor.toggleItalic)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Strikethrough}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('strike')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
|
||||
on:click={getToggler(textEditor.toggleStrike)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={Link}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('link')}
|
||||
disabled={isSelectionEmpty && !activeModes.has('link')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Link }}
|
||||
@ -175,14 +175,14 @@
|
||||
<div class="buttons-divider" />
|
||||
<StyleButton
|
||||
icon={ListNumber}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('orderedList')}
|
||||
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
|
||||
on:click={getToggler(textEditor.toggleOrderedList)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={ListBullet}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('bulletList')}
|
||||
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
|
||||
on:click={getToggler(textEditor.toggleBulletList)}
|
||||
@ -190,7 +190,7 @@
|
||||
<div class="buttons-divider" />
|
||||
<StyleButton
|
||||
icon={Quote}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('blockquote')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
|
||||
on:click={getToggler(textEditor.toggleBlockquote)}
|
||||
@ -198,14 +198,14 @@
|
||||
<div class="buttons-divider" />
|
||||
<StyleButton
|
||||
icon={Code}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('code')}
|
||||
showTooltip={{ label: textEditorPlugin.string.Code }}
|
||||
on:click={getToggler(textEditor.toggleCode)}
|
||||
/>
|
||||
<StyleButton
|
||||
icon={CodeBlock}
|
||||
size={'small'}
|
||||
size={buttonSize}
|
||||
selected={activeModes.has('codeBlock')}
|
||||
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
|
||||
on:click={getToggler(textEditor.toggleCodeBlock)}
|
||||
@ -254,9 +254,7 @@
|
||||
{#if showButtons}
|
||||
<div class="buttons">
|
||||
{#each defActions as a}
|
||||
<div class="p-1">
|
||||
<StyleButton icon={a.icon} size={'large'} on:click={(evt) => handleAction(a, evt)} />
|
||||
</div>
|
||||
<StyleButton icon={a.icon} size={buttonSize} on:click={(evt) => handleAction(a, evt)} />
|
||||
{/each}
|
||||
<div class="flex-grow">
|
||||
<slot />
|
||||
|
@ -510,6 +510,7 @@ input.search {
|
||||
.min-h-60 { min-height: 15rem; }
|
||||
.max-h-125 { max-height: 31.25rem; }
|
||||
.max-h-30 { max-height: 7.5rem; }
|
||||
.max-h-50 { max-height: 12.5rem; }
|
||||
.max-h-60 { max-height: 15rem; }
|
||||
.max-w-30 { max-width: 7.5rem; }
|
||||
.max-w-60 { max-width: 15rem; }
|
||||
|
@ -18,6 +18,7 @@
|
||||
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||
import { IconSize } from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import attachment from '../plugin'
|
||||
import { deleteFile, uploadFile } from '../utils'
|
||||
@ -30,6 +31,7 @@
|
||||
export let placeholder: IntlString | undefined = undefined
|
||||
export let alwaysEdit = false
|
||||
export let showButtons = false
|
||||
export let buttonSize: IconSize = 'small'
|
||||
export let maxHeight: 'max' | 'card' | string = 'max'
|
||||
|
||||
export function attach (): void {
|
||||
@ -191,7 +193,7 @@
|
||||
on:dragleave={() => {}}
|
||||
on:drop|preventDefault|stopPropagation={fileDrop}
|
||||
>
|
||||
<StyledTextBox bind:this={refInput} bind:content {placeholder} {alwaysEdit} {showButtons} {maxHeight} />
|
||||
<StyledTextBox bind:this={refInput} bind:content {placeholder} {alwaysEdit} {showButtons} {buttonSize} {maxHeight} />
|
||||
{#if attachments.size}
|
||||
<div class="flex-row-center list scroll-divider-color mt-1">
|
||||
{#each Array.from(attachments.values()) as attachment}
|
||||
|
@ -122,6 +122,7 @@
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
id="channel-ok"
|
||||
focusIndex={3}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
|
@ -305,6 +305,9 @@
|
||||
label: recruit.string.CreateVacancy
|
||||
}}
|
||||
bind:value={_space}
|
||||
on:change={(evt) => {
|
||||
_space = evt.detail
|
||||
}}
|
||||
component={VacancyOrgPresenter}
|
||||
componentProps={{ inline: true }}
|
||||
>
|
||||
|
@ -436,18 +436,32 @@
|
||||
bind:value={firstName}
|
||||
kind={'large-style'}
|
||||
focus
|
||||
maxWidth={'30rem'}
|
||||
focusIndex={1}
|
||||
/>
|
||||
<EditBox
|
||||
placeholder={recruit.string.PersonLastNamePlaceholder}
|
||||
bind:value={lastName}
|
||||
maxWidth={'30rem'}
|
||||
kind={'large-style'}
|
||||
focusIndex={2}
|
||||
/>
|
||||
<div class="mt-1">
|
||||
<EditBox placeholder={recruit.string.Title} bind:value={object.title} kind={'small-style'} focusIndex={3} />
|
||||
<EditBox
|
||||
placeholder={recruit.string.Title}
|
||||
bind:value={object.title}
|
||||
kind={'small-style'}
|
||||
focusIndex={3}
|
||||
maxWidth={'30rem'}
|
||||
/>
|
||||
</div>
|
||||
<EditBox placeholder={recruit.string.Location} bind:value={object.city} kind={'small-style'} focusIndex={4} />
|
||||
<EditBox
|
||||
placeholder={recruit.string.Location}
|
||||
bind:value={object.city}
|
||||
kind={'small-style'}
|
||||
focusIndex={4}
|
||||
maxWidth={'30rem'}
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<EditableAvatar
|
||||
|
@ -13,13 +13,15 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||
import contact, { Organization } from '@hcengineering/contact'
|
||||
import core, { Ref } from '@hcengineering/core'
|
||||
import core, { generateId, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { Card, getClient, UserBox } from '@hcengineering/presentation'
|
||||
import task, { createKanban, KanbanTemplate } from '@hcengineering/task'
|
||||
import { Button, Component, createFocusManager, EditBox, FocusHandler } from '@hcengineering/ui'
|
||||
import { Button, Component, createFocusManager, EditBox, FocusHandler, IconAttachment } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
import { Vacancy as VacancyClass } from '@hcengineering/recruit'
|
||||
import Company from './icons/Company.svelte'
|
||||
import Vacancy from './icons/Vacancy.svelte'
|
||||
|
||||
@ -27,7 +29,10 @@
|
||||
|
||||
let name: string = ''
|
||||
const description: string = ''
|
||||
let fullDescription: string = ''
|
||||
let templateId: Ref<KanbanTemplate> | undefined
|
||||
let objectId: Ref<VacancyClass> = generateId()
|
||||
|
||||
export let company: Ref<Organization> | undefined
|
||||
export let preserveCompany: boolean = false
|
||||
|
||||
@ -45,19 +50,31 @@
|
||||
throw Error(`Failed to find target kanban template: ${templateId}`)
|
||||
}
|
||||
|
||||
const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, {
|
||||
name,
|
||||
description,
|
||||
private: false,
|
||||
archived: false,
|
||||
company,
|
||||
members: []
|
||||
})
|
||||
const id = await client.createDoc(
|
||||
recruit.class.Vacancy,
|
||||
core.space.Space,
|
||||
{
|
||||
name,
|
||||
description,
|
||||
fullDescription,
|
||||
private: false,
|
||||
archived: false,
|
||||
company,
|
||||
members: [getCurrentAccount()._id]
|
||||
},
|
||||
objectId
|
||||
)
|
||||
|
||||
await createKanban(client, id, templateId)
|
||||
|
||||
await descriptionBox.createAttachments()
|
||||
objectId = generateId()
|
||||
|
||||
dispatch('close', id)
|
||||
}
|
||||
const manager = createFocusManager()
|
||||
|
||||
let descriptionBox: AttachmentStyledBox
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -83,7 +100,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
<svelte:fragment slot="header">
|
||||
<UserBox
|
||||
focusIndex={3}
|
||||
_class={contact.class.Organization}
|
||||
@ -100,6 +117,20 @@
|
||||
showNavigate={false}
|
||||
create={{ component: contact.component.CreateOrganization, label: contact.string.CreateOrganization }}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<AttachmentStyledBox
|
||||
bind:this={descriptionBox}
|
||||
{objectId}
|
||||
_class={recruit.class.Vacancy}
|
||||
space={objectId}
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
maxHeight={'card'}
|
||||
bind:content={fullDescription}
|
||||
placeholder={recruit.string.FullDescription}
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
<Component
|
||||
is={task.component.KanbanTemplateSelector}
|
||||
props={{
|
||||
@ -112,4 +143,13 @@
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
icon={IconAttachment}
|
||||
kind={'transparent'}
|
||||
on:click={() => {
|
||||
descriptionBox.attach()
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
@ -96,8 +96,7 @@
|
||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||
}
|
||||
const onContextMenu = (evt: any) => showMenu(evt.detail.evt, evt.detail.objects)
|
||||
$: query = { doneState: null, space }
|
||||
let resultQuery = query
|
||||
let resultQuery = { doneState: null, space }
|
||||
</script>
|
||||
|
||||
{#await cardPresenter then presenter}
|
||||
@ -106,8 +105,13 @@
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
<FilterBar {_class} {query} on:change={(e) => (resultQuery = e.detail)} />
|
||||
|
||||
<FilterBar
|
||||
{_class}
|
||||
query={{ doneState: null, space }}
|
||||
on:change={(e) => {
|
||||
resultQuery = e.detail
|
||||
}}
|
||||
/>
|
||||
<KanbanUI
|
||||
bind:this={kanbanUI}
|
||||
{_class}
|
||||
|
@ -217,7 +217,15 @@
|
||||
|
||||
"AddedReference": "Added reference",
|
||||
"AddedAsBlocked": "Marked as blocked",
|
||||
"AddedAsBlocking": "Marked as blocking"
|
||||
"AddedAsBlocking": "Marked as blocking",
|
||||
|
||||
"IssueTemplate": "Process",
|
||||
"IssueTemplates": "Process",
|
||||
"NewProcess": "New Process",
|
||||
"SaveProcess": "Save Process",
|
||||
"NoIssueTemplate": "No Process",
|
||||
"TemplateReplace": "You you replace applied template?",
|
||||
"TemplateReplaceConfirm": "All changes to template values will be lost."
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -217,7 +217,12 @@
|
||||
|
||||
"AddedReference": "Добавлена зависимость",
|
||||
"AddedAsBlocked": "Отмечено как заблокировано",
|
||||
"AddedAsBlocking": "Отмечено как блокирующее"
|
||||
"AddedAsBlocking": "Отмечено как блокирующее",
|
||||
|
||||
"IssueTemplate": "Процесс",
|
||||
"IssueTemplates": "Процессы",
|
||||
"NewProcess": "Новый процесс",
|
||||
"NoIssueTemplate": "Без процесса"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -18,9 +18,19 @@
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import core, { Account, AttachedData, Doc, generateId, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { Card, createQuery, getClient, KeyedAttribute, SpaceSelector } from '@hcengineering/presentation'
|
||||
import { Card, createQuery, getClient, KeyedAttribute, MessageBox, SpaceSelector } from '@hcengineering/presentation'
|
||||
import tags, { TagElement, TagReference } from '@hcengineering/tags'
|
||||
import { calcRank, Issue, IssuePriority, IssueStatus, Project, Sprint, Team } from '@hcengineering/tracker'
|
||||
import {
|
||||
calcRank,
|
||||
Issue,
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
IssueTemplate,
|
||||
IssueTemplateChild,
|
||||
Project,
|
||||
Sprint,
|
||||
Team
|
||||
} from '@hcengineering/tracker'
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
@ -35,6 +45,7 @@
|
||||
Spinner
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { ObjectBox } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { activeProject, activeSprint } from '../issues'
|
||||
import tracker from '../plugin'
|
||||
@ -47,6 +58,7 @@
|
||||
import SetDueDateActionPopup from './SetDueDateActionPopup.svelte'
|
||||
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
|
||||
import SprintSelector from './sprints/SprintSelector.svelte'
|
||||
import IssueTemplateChilds from './templates/IssueTemplateChilds.svelte'
|
||||
|
||||
export let space: Ref<Team>
|
||||
export let status: Ref<IssueStatus> | undefined = undefined
|
||||
@ -81,6 +93,69 @@
|
||||
childInfo: []
|
||||
}
|
||||
|
||||
function resetObject (): void {
|
||||
object = {
|
||||
title: '',
|
||||
description: '',
|
||||
assignee: assignee,
|
||||
project: project,
|
||||
sprint: sprint,
|
||||
number: 0,
|
||||
rank: '',
|
||||
status: '' as Ref<IssueStatus>,
|
||||
priority: priority,
|
||||
dueDate: null,
|
||||
comments: 0,
|
||||
subIssues: 0,
|
||||
parents: [],
|
||||
reportedTime: 0,
|
||||
estimation: 0,
|
||||
reports: 0,
|
||||
childInfo: []
|
||||
}
|
||||
subIssues = []
|
||||
}
|
||||
|
||||
let templateId: Ref<IssueTemplate> | undefined = undefined
|
||||
|
||||
let template: IssueTemplate | undefined = undefined
|
||||
const templateQuery = createQuery()
|
||||
|
||||
let subIssues: IssueTemplateChild[] = []
|
||||
|
||||
$: if (templateId !== undefined) {
|
||||
templateQuery.query(tracker.class.IssueTemplate, { _id: templateId }, (res) => {
|
||||
template = res.shift()
|
||||
})
|
||||
} else {
|
||||
template = undefined
|
||||
templateQuery.unsubscribe()
|
||||
}
|
||||
|
||||
function updateObject (template: IssueTemplate): void {
|
||||
if (object.template?.template === template._id) {
|
||||
return
|
||||
}
|
||||
|
||||
const { _class, _id, space, children, comments, attachments, labels, dueDate, ...templBase } = template
|
||||
|
||||
subIssues = template.children
|
||||
|
||||
object = {
|
||||
...object,
|
||||
...templBase,
|
||||
template: {
|
||||
template: template._id
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateTemplate (template?: IssueTemplate): void {
|
||||
if (template !== undefined) {
|
||||
updateObject(template)
|
||||
}
|
||||
}
|
||||
$: updateTemplate(template)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const statusesQuery = createQuery()
|
||||
@ -93,13 +168,20 @@
|
||||
}
|
||||
|
||||
$: _space = space
|
||||
$: updateIssueStatusId(space, status)
|
||||
$: updateIssueStatusId(_space, status)
|
||||
$: canSave = getTitle(object.title ?? '').length > 0
|
||||
|
||||
$: statusesQuery.query(tracker.class.IssueStatus, { attachedTo: _space }, (statuses) => (issueStatuses = statuses), {
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
})
|
||||
$: statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ attachedTo: _space },
|
||||
(statuses) => {
|
||||
issueStatuses = statuses
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
|
||||
async function updateIssueStatusId (teamId: Ref<Team>, issueStatusId?: Ref<IssueStatus>) {
|
||||
if (issueStatusId !== undefined) {
|
||||
@ -194,8 +276,73 @@
|
||||
await update(doc, 'relations', [relatedTo], tracker.string.AddedReference)
|
||||
}
|
||||
}
|
||||
for (const subIssue of subIssues) {
|
||||
const lastOne = await client.findOne<Issue>(
|
||||
tracker.class.Issue,
|
||||
{ status: object.status },
|
||||
{ sort: { rank: SortingOrder.Descending } }
|
||||
)
|
||||
const incResult = await client.updateDoc(
|
||||
tracker.class.Team,
|
||||
core.space.Space,
|
||||
_space,
|
||||
{
|
||||
$inc: { sequence: 1 }
|
||||
},
|
||||
true
|
||||
)
|
||||
const childId: Ref<Issue> = generateId()
|
||||
const cvalue: AttachedData<Issue> = {
|
||||
title: getTitle(subIssue.title),
|
||||
description: subIssue.description,
|
||||
assignee: subIssue.assignee,
|
||||
project: subIssue.project,
|
||||
sprint: subIssue.sprint,
|
||||
number: (incResult as any).object.sequence,
|
||||
status: object.status,
|
||||
priority: subIssue.priority,
|
||||
rank: calcRank(lastOne, undefined),
|
||||
comments: 0,
|
||||
subIssues: 0,
|
||||
dueDate: subIssue.dueDate,
|
||||
parents: parentIssue
|
||||
? [
|
||||
{ parentId: objectId, parentTitle: value.title },
|
||||
{ parentId: parentIssue._id, parentTitle: parentIssue.title },
|
||||
...parentIssue.parents
|
||||
]
|
||||
: [{ parentId: objectId, parentTitle: value.title }],
|
||||
reportedTime: 0,
|
||||
estimation: object.estimation,
|
||||
reports: 0,
|
||||
relations: [],
|
||||
childInfo: []
|
||||
}
|
||||
|
||||
await client.addCollection(
|
||||
tracker.class.Issue,
|
||||
_space,
|
||||
objectId,
|
||||
tracker.class.Issue,
|
||||
'subIssues',
|
||||
cvalue,
|
||||
childId
|
||||
)
|
||||
|
||||
if ((subIssue.labels?.length ?? 0) > 0) {
|
||||
const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: subIssue.labels } })
|
||||
for (const label of tagElements) {
|
||||
await client.addCollection(tags.class.TagReference, _space, childId, tracker.class.Issue, 'labels', {
|
||||
title: label.title,
|
||||
color: label.color,
|
||||
tag: label._id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objectId = generateId()
|
||||
resetObject()
|
||||
}
|
||||
|
||||
async function showMoreActions (ev: Event) {
|
||||
@ -275,6 +422,31 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
function handleTemplateChange (evt: CustomEvent<Ref<IssueTemplate>>): void {
|
||||
if (templateId == null) {
|
||||
templateId = evt.detail
|
||||
return
|
||||
}
|
||||
// Template is already specified, ask to replace.
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: tracker.string.TemplateReplace,
|
||||
message: tracker.string.TemplateReplaceConfirm
|
||||
},
|
||||
'top',
|
||||
(result?: boolean) => {
|
||||
if (result === true) {
|
||||
templateId = evt.detail ?? undefined
|
||||
|
||||
if (templateId === undefined) {
|
||||
subIssues = []
|
||||
resetObject()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@ -299,6 +471,18 @@
|
||||
disabled
|
||||
on:click={() => {}}
|
||||
/> -->
|
||||
<ObjectBox
|
||||
_class={tracker.class.IssueTemplate}
|
||||
value={templateId}
|
||||
on:change={handleTemplateChange}
|
||||
size={'small'}
|
||||
label={tracker.string.NoIssueTemplate}
|
||||
icon={tracker.icon.Issues}
|
||||
searchField={'title'}
|
||||
allowDeselect={true}
|
||||
showNavigate={false}
|
||||
docProps={{ disableClick: true }}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title" let:label>
|
||||
<div class="flex-row-center gap-1">
|
||||
@ -320,17 +504,20 @@
|
||||
<ParentIssue issue={parentIssue} on:close={clearParentIssue} />
|
||||
{/if}
|
||||
<EditBox bind:value={object.title} placeholder={tracker.string.IssueTitlePlaceholder} kind={'large-style'} focus />
|
||||
<AttachmentStyledBox
|
||||
bind:this={descriptionBox}
|
||||
{objectId}
|
||||
_class={tracker.class.Issue}
|
||||
space={_space}
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
maxHeight={'card'}
|
||||
bind:content={object.description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
{#key object.description}
|
||||
<AttachmentStyledBox
|
||||
bind:this={descriptionBox}
|
||||
{objectId}
|
||||
_class={tracker.class.Issue}
|
||||
space={_space}
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
maxHeight={'card'}
|
||||
bind:content={object.description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
{/key}
|
||||
<IssueTemplateChilds bind:children={subIssues} sprint={object.sprint} project={object.project} />
|
||||
<svelte:fragment slot="pool">
|
||||
<div class="flex flex-wrap" style:gap={'0.2vw'}>
|
||||
{#if issueStatuses}
|
||||
|
@ -13,15 +13,15 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { AttachedData, Ref } from '@hcengineering/core'
|
||||
import { getClient, EmployeeBox } from '@hcengineering/presentation'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { EmployeeBox, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueTemplateData } from '@hcengineering/tracker'
|
||||
import { ButtonKind, ButtonSize, TooltipAlignment } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: Issue | AttachedData<Issue>
|
||||
export let value: Issue | AttachedData<Issue> | IssueTemplateData
|
||||
export let size: ButtonSize = 'large'
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let tooltipAlignment: TooltipAlignment | undefined = undefined
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { Issue, IssueTemplate } from '@hcengineering/tracker'
|
||||
import { UsersPopup, getClient } from '@hcengineering/presentation'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
@ -25,6 +25,7 @@
|
||||
|
||||
export let value: Employee | null | undefined
|
||||
export let issueId: Ref<Issue>
|
||||
export let issueClass: Ref<Class<Issue | IssueTemplate>> = tracker.class.Issue
|
||||
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
@ -51,7 +52,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
const currentIssue = await client.findOne(tracker.class.Issue, { _id: issueId })
|
||||
const currentIssue = await client.findOne(issueClass, { _id: issueId })
|
||||
|
||||
if (currentIssue === undefined) {
|
||||
return
|
||||
@ -59,15 +60,7 @@
|
||||
|
||||
const newAssignee = result === null ? null : result._id
|
||||
|
||||
await client.updateCollection(
|
||||
currentIssue._class,
|
||||
currentIssue.space,
|
||||
currentIssue._id,
|
||||
currentIssue.attachedTo,
|
||||
currentIssue.attachedToClass,
|
||||
currentIssue.collection,
|
||||
{ assignee: newAssignee }
|
||||
)
|
||||
await client.update(currentIssue, { assignee: newAssignee })
|
||||
}
|
||||
|
||||
const handleAssigneeEditorOpened = async (event: MouseEvent) => {
|
||||
|
@ -18,7 +18,8 @@
|
||||
import notification from '@hcengineering/notification'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import {
|
||||
import ui, {
|
||||
ActionIcon,
|
||||
Button,
|
||||
CheckBox,
|
||||
Component,
|
||||
@ -26,6 +27,7 @@
|
||||
ExpandCollapse,
|
||||
getEventPositionElement,
|
||||
IconAdd,
|
||||
IconMoreH,
|
||||
showPopup,
|
||||
Spinner,
|
||||
tooltip
|
||||
@ -36,7 +38,6 @@
|
||||
import tracker from '../../plugin'
|
||||
import { IssuesGroupByKeys, issuesGroupEditorMap, IssuesOrderByKeys, issuesSortOrderMap } from '../../utils'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
import GrowPresenter from './GrowPresenter.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
@ -75,13 +76,19 @@
|
||||
|
||||
let personPresenter: AttributeModel
|
||||
|
||||
const defaultLimit = 20
|
||||
const autoFoldLimit = 20
|
||||
const singleCategoryLimit = 200
|
||||
|
||||
const isCollapsedMap: Record<any, boolean> = {}
|
||||
|
||||
const noCategory = '#no_category'
|
||||
|
||||
$: {
|
||||
const exkeys = new Set(Object.keys(isCollapsedMap))
|
||||
for (const c of categories) {
|
||||
if (!exkeys.delete(c)) {
|
||||
isCollapsedMap[c] = false
|
||||
if (!exkeys.delete(toCat(c))) {
|
||||
isCollapsedMap[toCat(c)] = categories.length === 1 ? false : (groupedIssues[c]?.length ?? 0) > autoFoldLimit
|
||||
}
|
||||
}
|
||||
for (const k of exkeys) {
|
||||
@ -160,7 +167,13 @@
|
||||
)
|
||||
}
|
||||
|
||||
const handleCollapseCategory = (category: any) => (isCollapsedMap[category] = !isCollapsedMap[category])
|
||||
function toCat (category: any): any {
|
||||
return category ?? noCategory
|
||||
}
|
||||
|
||||
const handleCollapseCategory = (category: any) => {
|
||||
isCollapsedMap[category] = !isCollapsedMap[category]
|
||||
}
|
||||
|
||||
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
|
||||
if (options?.limit && options?.limit > 0) {
|
||||
@ -181,12 +194,27 @@
|
||||
const checkWidth = (key: string, result: CustomEvent): void => {
|
||||
if (result !== undefined) propsWidth[key] = result.detail
|
||||
}
|
||||
function limitGroup (
|
||||
category: any,
|
||||
groupes: { [key: string | number | symbol]: Issue[] },
|
||||
categoryLimit: Record<any, number>
|
||||
): Issue[] {
|
||||
const issues = groupes[category] ?? []
|
||||
if (Object.keys(groupes).length === 1) {
|
||||
return issues.slice(0, singleCategoryLimit)
|
||||
}
|
||||
const limit = categoryLimit[toCat(category)] ?? defaultLimit
|
||||
return issues.slice(0, limit)
|
||||
}
|
||||
const categoryLimit: Record<any, number> = {}
|
||||
</script>
|
||||
|
||||
<div class="issueslist-container" style={varsStyle}>
|
||||
{#each categories as category}
|
||||
{@const items = groupedIssues[category] ?? []}
|
||||
{@const limited = limitGroup(category, groupedIssues, categoryLimit) ?? []}
|
||||
{#if headerComponent || groupByKey === 'assignee'}
|
||||
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(category)}>
|
||||
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(toCat(category))}>
|
||||
<div class="flex-row-center gap-2 clear-mins">
|
||||
{#if groupByKey === 'assignee' && personPresenter}
|
||||
<svelte:component
|
||||
@ -216,17 +244,29 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<span class="text-base content-dark-color ml-4">{(groupedIssues[category] ?? []).length}</span>
|
||||
{#if limited.length < items.length}
|
||||
<span class="text-base content-dark-color ml-4"> {limited.length} / {items.length}</span>
|
||||
<ActionIcon
|
||||
size={'small'}
|
||||
icon={IconMoreH}
|
||||
label={ui.string.ShowMore}
|
||||
action={() => {
|
||||
categoryLimit[toCat(category)] = limited.length + 20
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<span class="text-base content-dark-color ml-4">{items.length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="clear-mins" use:tooltip={{ label: tracker.string.AddIssueTooltip }}>
|
||||
<Button icon={IconAdd} kind={'transparent'} on:click={(event) => handleNewIssueAdded(event, category)} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<ExpandCollapse isExpanded={!isCollapsedMap[category]} duration={400}>
|
||||
<ExpandCollapse isExpanded={!isCollapsedMap[toCat(category)]} duration={400}>
|
||||
{#if itemModels}
|
||||
{#if groupedIssues[category]}
|
||||
{#each groupedIssues[category] as docObject (docObject._id)}
|
||||
{#each limited as docObject (docObject._id)}
|
||||
<div
|
||||
bind:this={objectRefs[combinedGroupedIssues.findIndex((x) => x === docObject)]}
|
||||
class="listGrid antiList__row row gap-2 flex-grow"
|
||||
@ -262,8 +302,8 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#each itemModels as attributeModel, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
{#each itemModels as attributeModel}
|
||||
{#if attributeModel.props?.type === 'priority'}
|
||||
<div class="priorityPresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
@ -274,7 +314,7 @@
|
||||
{currentTeam}
|
||||
/>
|
||||
</div>
|
||||
{:else if attributeModelIndex === 1}
|
||||
{:else if attributeModel.props?.type === 'issue'}
|
||||
<div class="issuePresenter">
|
||||
<FixedColumn
|
||||
width={propsWidth.issue}
|
||||
@ -292,7 +332,7 @@
|
||||
/>
|
||||
</FixedColumn>
|
||||
</div>
|
||||
{:else if attributeModelIndex === 3 || attributeModel.presenter === GrowPresenter}
|
||||
{:else if attributeModel.props?.type === 'grow'}
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
|
@ -13,16 +13,24 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AttachedData } from '@hcengineering/core'
|
||||
import { Issue, IssuePriority } from '@hcengineering/tracker'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, eventToHTMLElement } from '@hcengineering/ui'
|
||||
import { ButtonKind, ButtonSize, showPopup, SelectPopup, Icon, Label } from '@hcengineering/ui'
|
||||
import { Issue, IssuePriority, IssueTemplateData } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
eventToHTMLElement,
|
||||
Icon,
|
||||
Label,
|
||||
SelectPopup,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { defaultPriorities, issuePriorities } from '../../utils'
|
||||
|
||||
export let value: Issue | AttachedData<Issue>
|
||||
export let value: Issue | AttachedData<Issue> | IssueTemplateData
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
|
||||
|
@ -13,15 +13,15 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AttachedData, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, showPopup, SelectPopup, TooltipAlignment, eventToHTMLElement } from '@hcengineering/ui'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
|
||||
export let value: Issue | AttachedData<Issue>
|
||||
export let statuses: WithLookup<IssueStatus>[] | undefined = undefined
|
||||
|
@ -181,16 +181,6 @@
|
||||
on:close={() => dispatch('close')}
|
||||
>
|
||||
{@const { attachedTo: parentIssue } = issue?.$lookup ?? {}}
|
||||
<svelte:fragment slot="subtitle">
|
||||
<div class="flex-between flex-grow">
|
||||
<div class="buttons-group xsmall-gap">
|
||||
<Button icon={IconEdit} kind={'transparent'} size="medium" on:click={edit} />
|
||||
{#if innerWidth < 900}
|
||||
<Button icon={IconMoreH} kind={'transparent'} size="medium" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="navigator">
|
||||
<UpDownNavigator element={issue} />
|
||||
</svelte:fragment>
|
||||
|
@ -17,12 +17,12 @@
|
||||
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { Button, ButtonKind, ButtonSize, eventToHTMLElement, Label, showPopup } from '@hcengineering/ui'
|
||||
import { Button, ButtonKind, ButtonSize, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import EditBoxPopup from '@hcengineering/view-resources/src/components/EditBoxPopup.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationPopup from './EstimationPopup.svelte'
|
||||
import EstimationProgressCircle from './EstimationProgressCircle.svelte'
|
||||
import EstimationStatsPresenter from './EstimationStatsPresenter.svelte'
|
||||
|
||||
export let value: Issue | AttachedData<Issue>
|
||||
export let isEditable: boolean = true
|
||||
@ -43,16 +43,11 @@
|
||||
}
|
||||
|
||||
if (kind === 'list') {
|
||||
showPopup(
|
||||
EstimationPopup,
|
||||
{ value: value.estimation, format: 'number', object: value },
|
||||
eventToHTMLElement(event),
|
||||
(res) => {
|
||||
if (res != null) {
|
||||
changeEstimation(res)
|
||||
}
|
||||
showPopup(EstimationPopup, { value: value.estimation, format: 'number', object: value }, 'top', (res) => {
|
||||
if (res != null) {
|
||||
changeEstimation(res)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
showPopup(EditBoxPopup, { value, format: 'number' }, eventToHTMLElement(event), (res) => {
|
||||
if (res !== undefined) {
|
||||
@ -75,65 +70,11 @@
|
||||
value.estimation = newEstimation
|
||||
}
|
||||
}
|
||||
|
||||
$: childReportTime = (value.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0)
|
||||
$: childEstimationTime = (value.childInfo ?? []).map((it) => it.estimation).reduce((a, b) => a + b, 0)
|
||||
|
||||
function hourFloor (value: number): number {
|
||||
const days = Math.ceil(value)
|
||||
const hours = value - days
|
||||
return days + Math.floor(hours * 100) / 100
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if kind === 'list'}
|
||||
<div class="estimation-container" on:click={handleestimationEditorOpened}>
|
||||
<div class="icon">
|
||||
<EstimationProgressCircle value={Math.max(value.reportedTime, childReportTime)} max={value.estimation} />
|
||||
</div>
|
||||
<span class="overflow-label label flex-row-center flex-nowrap text-md">
|
||||
{#if value.reportedTime > 0 || childReportTime > 0}
|
||||
{#if childReportTime}
|
||||
{@const rchildReportTime = hourFloor(childReportTime)}
|
||||
{@const reportDiff = rchildReportTime - hourFloor(value.reportedTime)}
|
||||
{#if reportDiff !== 0 && value.reportedTime !== 0}
|
||||
<div class="flex flex-nowrap mr-1" class:showError={reportDiff > 0}>
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: rchildReportTime }} />
|
||||
</div>
|
||||
<div class="romColor">
|
||||
(<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />)
|
||||
</div>
|
||||
{:else if value.reportedTime === 0}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(childReportTime) }} />
|
||||
{:else}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />
|
||||
{/if}
|
||||
<div class="p-1">/</div>
|
||||
{/if}
|
||||
{#if childEstimationTime}
|
||||
{@const childEstTime = Math.round(childEstimationTime)}
|
||||
{@const estimationDiff = childEstTime - Math.round(value.estimation)}
|
||||
{#if estimationDiff !== 0}
|
||||
<div class="flex flex-nowrap mr-1" class:showWarning={estimationDiff !== 0}>
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: childEstTime }} />
|
||||
</div>
|
||||
{#if value.estimation !== 0}
|
||||
<div class="romColor">
|
||||
(<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />)
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<EstimationStatsPresenter {value} on:click={handleestimationEditorOpened} />
|
||||
{:else}
|
||||
<Button
|
||||
showTooltip={isEditable ? { label: tracker.string.Estimation } : undefined}
|
||||
@ -149,44 +90,3 @@
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.estimation-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--content-color);
|
||||
}
|
||||
.label {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
&:hover {
|
||||
.icon {
|
||||
color: var(--caption-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.showError {
|
||||
color: var(--error-color) !important;
|
||||
}
|
||||
.showWarning {
|
||||
color: var(--warning-color) !important;
|
||||
}
|
||||
.romColor {
|
||||
color: var(--content-color) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -14,28 +14,17 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { FindOptions } from '@hcengineering/core'
|
||||
import presentation, { Card } from '@hcengineering/presentation'
|
||||
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
EditBox,
|
||||
EditStyle,
|
||||
eventToHTMLElement,
|
||||
IconAdd,
|
||||
Label,
|
||||
Scroller,
|
||||
tableSP,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { TableBrowser } from '@hcengineering/view-resources'
|
||||
import { SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import presentation, { Card, createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Button, EditBox, EditStyle, eventToHTMLElement, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../../plugin'
|
||||
import IssuePresenter from '../IssuePresenter.svelte'
|
||||
import ParentNamesPresenter from '../ParentNamesPresenter.svelte'
|
||||
import EstimationPresenter from './EstimationPresenter.svelte'
|
||||
import EstimationStatsPresenter from './EstimationStatsPresenter.svelte'
|
||||
import SubIssuesEstimations from './SubIssuesEstimations.svelte'
|
||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||
import TimeSpendReports from './TimeSpendReports.svelte'
|
||||
|
||||
export let value: string | number | undefined
|
||||
export let format: 'text' | 'password' | 'number'
|
||||
@ -49,10 +38,43 @@
|
||||
function _onkeypress (ev: KeyboardEvent) {
|
||||
if (ev.key === 'Enter') dispatch('close', _value)
|
||||
}
|
||||
const options: FindOptions<TimeSpendReport> = {
|
||||
lookup: { employee: contact.class.Employee, attachedTo: tracker.class.Issue }
|
||||
}
|
||||
|
||||
$: childIds = Array.from((object.childInfo ?? []).map((it) => it.childId))
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
let currentTeam: Team | undefined
|
||||
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||
|
||||
$: query.query(
|
||||
object._class,
|
||||
{ _id: object._id },
|
||||
(res) => {
|
||||
const r = res.shift()
|
||||
if (r !== undefined) {
|
||||
object = r
|
||||
currentTeam = r.$lookup?.space
|
||||
}
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
space: tracker.class.Team
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const statusesQuery = createQuery()
|
||||
|
||||
$: currentTeam &&
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ attachedTo: currentTeam._id },
|
||||
(statuses) => (issueStatuses = statuses),
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@ -66,6 +88,15 @@
|
||||
dispatch('close', null)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center">
|
||||
<Label label={tracker.string.Estimation} />
|
||||
<div class="ml-2">
|
||||
<EstimationStatsPresenter value={object} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="header">
|
||||
<IssuePresenter value={object} disableClick />
|
||||
</svelte:fragment>
|
||||
@ -78,49 +109,29 @@
|
||||
placeholder={tracker.string.Estimation}
|
||||
focus
|
||||
on:keypress={_onkeypress}
|
||||
on:change={() => {
|
||||
if (typeof _value === 'number') {
|
||||
object.estimation = _value
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Label label={tracker.string.ChildEstimation} />:
|
||||
<div class="h-50">
|
||||
<Scroller fade={tableSP}>
|
||||
<TableBrowser
|
||||
showFilterBar={false}
|
||||
_class={tracker.class.Issue}
|
||||
query={{ _id: { $in: childIds } }}
|
||||
config={[
|
||||
'',
|
||||
{ key: 'estimation', presenter: EstimationPresenter, label: tracker.string.Estimation },
|
||||
{ key: '', presenter: ParentNamesPresenter, props: { maxWidth: '20rem' }, label: tracker.string.Title }
|
||||
]}
|
||||
{options}
|
||||
/>
|
||||
</Scroller>
|
||||
</div>
|
||||
<Label label={tracker.string.ReportedTime} />:
|
||||
<div class="h-50">
|
||||
<Scroller fade={tableSP}>
|
||||
<TableBrowser
|
||||
_class={tracker.class.TimeSpendReport}
|
||||
query={{ attachedTo: { $in: [object._id, ...childIds] } }}
|
||||
showFilterBar={false}
|
||||
config={[
|
||||
'$lookup.attachedTo',
|
||||
'',
|
||||
'$lookup.employee',
|
||||
{
|
||||
key: '$lookup.attachedTo',
|
||||
presenter: ParentNamesPresenter,
|
||||
props: { maxWidth: '20rem' },
|
||||
label: tracker.string.Title
|
||||
},
|
||||
'date',
|
||||
'description'
|
||||
]}
|
||||
{options}
|
||||
/>
|
||||
</Scroller>
|
||||
</div>
|
||||
{#if currentTeam && issueStatuses}
|
||||
<SubIssuesEstimations
|
||||
issue={object}
|
||||
issueStatuses={new Map([[currentTeam._id, issueStatuses]])}
|
||||
teams={new Map([[currentTeam?._id, currentTeam]])}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if currentTeam}
|
||||
<TimeSpendReports
|
||||
issue={object}
|
||||
teams={new Map([[currentTeam?._id, currentTeam]])}
|
||||
query={{ attachedTo: { $in: [object._id, ...childIds] } }}
|
||||
/>
|
||||
{/if}
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
|
@ -0,0 +1,122 @@
|
||||
<!--
|
||||
// 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 { AttachedData } from '@hcengineering/core'
|
||||
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationProgressCircle from './EstimationProgressCircle.svelte'
|
||||
|
||||
export let value: Issue | AttachedData<Issue>
|
||||
|
||||
$: childReportTime =
|
||||
value.reportedTime + (value.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0)
|
||||
$: childEstimationTime = (value.childInfo ?? []).map((it) => it.estimation).reduce((a, b) => a + b, 0)
|
||||
|
||||
function hourFloor (value: number): number {
|
||||
const days = Math.ceil(value)
|
||||
const hours = value - days
|
||||
return days + Math.floor(hours * 100) / 100
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="estimation-container" on:click>
|
||||
<div class="icon">
|
||||
<EstimationProgressCircle value={Math.max(value.reportedTime, childReportTime)} max={value.estimation} />
|
||||
</div>
|
||||
<span class="overflow-label label flex-row-center flex-nowrap text-md">
|
||||
{#if value.reportedTime > 0 || childReportTime > 0}
|
||||
{#if childReportTime}
|
||||
{@const rchildReportTime = hourFloor(childReportTime)}
|
||||
{@const reportDiff = rchildReportTime - hourFloor(value.reportedTime)}
|
||||
{#if reportDiff !== 0 && value.reportedTime !== 0}
|
||||
<div class="flex flex-nowrap mr-1" class:showError={reportDiff > 0}>
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: rchildReportTime }} />
|
||||
</div>
|
||||
<div class="romColor">
|
||||
(<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />)
|
||||
</div>
|
||||
{:else if value.reportedTime === 0}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(childReportTime) }} />
|
||||
{:else}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />
|
||||
{/if}
|
||||
<div class="p-1">/</div>
|
||||
{/if}
|
||||
{#if childEstimationTime}
|
||||
{@const childEstTime = Math.round(childEstimationTime)}
|
||||
{@const estimationDiff = childEstTime - Math.round(value.estimation)}
|
||||
{#if estimationDiff !== 0}
|
||||
<div class="flex flex-nowrap mr-1" class:showWarning={estimationDiff !== 0}>
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: childEstTime }} />
|
||||
</div>
|
||||
{#if value.estimation !== 0}
|
||||
<div class="romColor">
|
||||
(<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />)
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.estimation-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--content-color);
|
||||
}
|
||||
.label {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
&:hover {
|
||||
.icon {
|
||||
color: var(--caption-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.showError {
|
||||
color: var(--error-color) !important;
|
||||
}
|
||||
.showWarning {
|
||||
color: var(--warning-color) !important;
|
||||
}
|
||||
.romColor {
|
||||
color: var(--content-color) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,114 @@
|
||||
<!--
|
||||
// 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 contact from '@hcengineering/contact'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { UserBox } from '@hcengineering/presentation'
|
||||
import { Issue, Team } from '@hcengineering/tracker'
|
||||
import { getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationPresenter from './EstimationPresenter.svelte'
|
||||
|
||||
export let issues: Issue[]
|
||||
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
|
||||
function showContextMenu (ev: MouseEvent, object: Issue) {
|
||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||
|
||||
let varsStyle: string = ''
|
||||
const propsWidth: Record<string, number> = { issue: 0 }
|
||||
$: if (propsWidth) {
|
||||
varsStyle = ''
|
||||
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
||||
}
|
||||
const checkWidth = (key: string, result: CustomEvent): void => {
|
||||
if (result !== undefined) propsWidth[key] = result.detail
|
||||
}
|
||||
</script>
|
||||
|
||||
<ListView count={issues.length}>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const issue = issues[item]}
|
||||
{@const currentTeam = teams.get(issue.space)}
|
||||
<div
|
||||
class="flex-between row"
|
||||
style={varsStyle}
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
|
||||
on:mouseover={() => {
|
||||
listProvider.updateFocus(issue)
|
||||
}}
|
||||
on:focus={() => {
|
||||
listProvider.updateFocus(issue)
|
||||
}}
|
||||
>
|
||||
<div class="flex-row-center clear-mins gap-2 p-2">
|
||||
<span class="issuePresenter">
|
||||
<FixedColumn
|
||||
width={propsWidth.issue}
|
||||
key={'issue'}
|
||||
justify={'left'}
|
||||
on:update={(result) => checkWidth('issue', result)}
|
||||
>
|
||||
{#if currentTeam}
|
||||
{getIssueId(currentTeam, issue)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
</span>
|
||||
<span class="text name" title={issue.title}>
|
||||
{issue.title}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-center flex-no-shrink">
|
||||
<UserBox
|
||||
label={tracker.string.Assignee}
|
||||
_class={contact.class.Employee}
|
||||
value={issue.assignee}
|
||||
readonly
|
||||
showNavigate={false}
|
||||
/>
|
||||
<EstimationPresenter value={issue.estimation} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
.text {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
font-weight: 500;
|
||||
color: var(--content-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,62 @@
|
||||
<!--
|
||||
// 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 { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Label, Scroller, Spinner } from '@hcengineering/ui'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationSubIssueList from './EstimationSubIssueList.svelte'
|
||||
|
||||
export let issue: Issue
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
|
||||
const subIssuesQuery = createQuery()
|
||||
|
||||
let subIssues: Issue[] | undefined
|
||||
|
||||
$: hasSubIssues = issue.subIssues > 0
|
||||
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
|
||||
sort: { estimation: SortingOrder.Descending }
|
||||
})
|
||||
$: total = (subIssues ?? []).reduce((a, b) => a + b.estimation, 0)
|
||||
</script>
|
||||
|
||||
{#if subIssues && issueStatuses}
|
||||
{#if hasSubIssues}
|
||||
<Label label={tracker.string.ChildEstimation} />: {total}
|
||||
<div class="h-50">
|
||||
<Scroller>
|
||||
<EstimationSubIssueList issues={subIssues} {teams} />
|
||||
</Scroller>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex-center pt-3">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.list {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
||||
&.collapsed {
|
||||
padding-top: 1px;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -84,6 +84,7 @@
|
||||
label={contact.string.Employee}
|
||||
kind={'link-bordered'}
|
||||
bind:value={data.employee}
|
||||
showNavigate={false}
|
||||
/>
|
||||
<DatePresenter kind={'link'} bind:value={data.date} editable />
|
||||
</div>
|
||||
|
@ -0,0 +1,64 @@
|
||||
<!--
|
||||
// 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 { DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, Team, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { Label, Scroller, Spinner } from '@hcengineering/ui'
|
||||
import tracker from '../../../plugin'
|
||||
import TimeSpendReportsList from './TimeSpendReportsList.svelte'
|
||||
|
||||
export let issue: Issue
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
export let query: DocumentQuery<TimeSpendReport>
|
||||
|
||||
const subIssuesQuery = createQuery()
|
||||
|
||||
let reports: TimeSpendReport[] | undefined
|
||||
|
||||
$: subIssuesQuery.query(tracker.class.TimeSpendReport, query, async (result) => (reports = result), {
|
||||
sort: { modifiedOn: SortingOrder.Descending },
|
||||
lookup: {
|
||||
attachedTo: tracker.class.Issue
|
||||
}
|
||||
})
|
||||
|
||||
$: total = (reports ?? []).reduce((a, b) => a + b.value, 0)
|
||||
</script>
|
||||
|
||||
{#if reports}
|
||||
<Label label={tracker.string.ReportedTime} />: {issue.reportedTime}
|
||||
<Label label={tracker.string.TimeSpendReports} />: {total}
|
||||
<div class="h-50">
|
||||
<Scroller>
|
||||
<TimeSpendReportsList {reports} {teams} />
|
||||
</Scroller>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-center pt-3">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.list {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
||||
&.collapsed {
|
||||
padding-top: 1px;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,129 @@
|
||||
<!--
|
||||
// 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 contact from '@hcengineering/contact'
|
||||
import { Doc, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import UserBox from '@hcengineering/presentation/src/components/UserBox.svelte'
|
||||
import { Team, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { eventToHTMLElement, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
|
||||
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationPresenter from './EstimationPresenter.svelte'
|
||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||
|
||||
export let reports: WithLookup<TimeSpendReport>[]
|
||||
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
|
||||
function showContextMenu (ev: MouseEvent, object: TimeSpendReport) {
|
||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||
|
||||
let varsStyle: string = ''
|
||||
const propsWidth: Record<string, number> = { issue: 0 }
|
||||
$: if (propsWidth) {
|
||||
varsStyle = ''
|
||||
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
||||
}
|
||||
const checkWidth = (key: string, result: CustomEvent): void => {
|
||||
if (result !== undefined) propsWidth[key] = result.detail
|
||||
}
|
||||
const toTeamId = (ref: Ref<Space>) => ref as Ref<Team>
|
||||
|
||||
function editSpendReport (event: MouseEvent, value: TimeSpendReport): void {
|
||||
showPopup(
|
||||
TimeSpendReportPopup,
|
||||
{ issue: value.attachedTo, issueClass: value.attachedToClass, value: value, assignee: value.employee },
|
||||
eventToHTMLElement(event)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ListView count={reports.length}>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const report = reports[item]}
|
||||
{@const currentTeam = teams.get(toTeamId(report.space))}
|
||||
<div
|
||||
class="flex-between row"
|
||||
style={varsStyle}
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, report)}
|
||||
on:mouseover={() => {
|
||||
listProvider.updateFocus(report)
|
||||
}}
|
||||
on:focus={() => {
|
||||
listProvider.updateFocus(report)
|
||||
}}
|
||||
on:click={(evt) => editSpendReport(evt, report)}
|
||||
>
|
||||
<div class="flex-row-center clear-mins gap-2 p-2">
|
||||
<span class="issuePresenter">
|
||||
<FixedColumn
|
||||
width={propsWidth.issue}
|
||||
key={'issue'}
|
||||
justify={'left'}
|
||||
on:update={(result) => checkWidth('issue', result)}
|
||||
>
|
||||
{#if currentTeam && report.$lookup?.attachedTo}
|
||||
{getIssueId(currentTeam, report.$lookup?.attachedTo)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
</span>
|
||||
{#if report.$lookup?.attachedTo?.title}
|
||||
<span class="text name" title={report.$lookup?.attachedTo?.title}>
|
||||
{report.$lookup?.attachedTo?.title}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-center flex-no-shrink">
|
||||
<UserBox
|
||||
label={tracker.string.Assignee}
|
||||
_class={contact.class.Employee}
|
||||
value={report.employee}
|
||||
readonly
|
||||
showNavigate={false}
|
||||
/>
|
||||
<EstimationPresenter value={report.value} />
|
||||
<DatePresenter value={report.modifiedOn} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
.text {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
font-weight: 500;
|
||||
color: var(--content-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -13,16 +13,18 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { DocumentQuery, getCurrentAccount, Ref, TxCollectionCUD } from '@hcengineering/core'
|
||||
import type { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import type { EmployeeAccount } from '@hcengineering/contact'
|
||||
import core, { DocumentQuery, getCurrentAccount, Ref, TxCollectionCUD } from '@hcengineering/core'
|
||||
import notification from '@hcengineering/notification'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import notification from '@hcengineering/notification'
|
||||
import type { Issue } from '@hcengineering/tracker'
|
||||
import { ViewOptionModel } from '@hcengineering/view-resources'
|
||||
|
||||
import tracker from '../../plugin'
|
||||
import { getDefaultViewOptionsConfig } from '../../utils'
|
||||
import IssuesView from '../issues/IssuesView.svelte'
|
||||
import ModeSelector from '../ModeSelector.svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
const config: [string, IntlString][] = [
|
||||
['assigned', tracker.string.Assigned],
|
||||
@ -30,18 +32,12 @@
|
||||
['subscribed', tracker.string.Subscribed]
|
||||
]
|
||||
const currentUser = getCurrentAccount() as EmployeeAccount
|
||||
let assigned = { assignee: currentUser.employee, status: { $in: [] as Ref<IssueStatus>[] } }
|
||||
const assigned = { assignee: currentUser.employee }
|
||||
let created = { _id: { $in: [] as Ref<Issue>[] } }
|
||||
let subscribed = { _id: { $in: [] as Ref<Issue>[] } }
|
||||
|
||||
const statusQuery = createQuery()
|
||||
$: statusQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ category: { $in: [tracker.issueStatusCategory.Started, tracker.issueStatusCategory.Unstarted] } },
|
||||
(result) => {
|
||||
assigned = { ...assigned, status: { $in: result.map(({ _id }) => _id) } }
|
||||
}
|
||||
)
|
||||
const viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsConfig(false)
|
||||
|
||||
const createdQuery = createQuery()
|
||||
$: createdQuery.query<TxCollectionCUD<Issue, Issue>>(
|
||||
core.class.TxCollectionCUD,
|
||||
@ -83,7 +79,7 @@
|
||||
$: query = getQuery(mode, { assigned, created, subscribed })
|
||||
</script>
|
||||
|
||||
<IssuesView {query} title={tracker.string.MyIssues}>
|
||||
<IssuesView {query} title={tracker.string.MyIssues} {viewOptionsConfig}>
|
||||
<svelte:fragment slot="afterHeader">
|
||||
<ModeSelector {config} {mode} onChange={handleChangeMode} />
|
||||
</svelte:fragment>
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import { Issue, IssueTemplate, Project } from '@hcengineering/tracker'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { ButtonKind, ButtonShape, ButtonSize, tooltip } from '@hcengineering/ui'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
@ -22,7 +22,7 @@
|
||||
import ProjectSelector from '../ProjectSelector.svelte'
|
||||
import { activeProject } from '../../issues'
|
||||
|
||||
export let value: Issue
|
||||
export let value: Issue | IssueTemplate
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let popupPlaceholder: IntlString = tracker.string.MoveToProject
|
||||
@ -43,15 +43,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateCollection(
|
||||
value._class,
|
||||
value.space,
|
||||
value._id,
|
||||
value.attachedTo,
|
||||
value.attachedToClass,
|
||||
value.collection,
|
||||
{ project: newProjectId }
|
||||
)
|
||||
await client.update(value, { project: newProjectId })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Sprint } from '@hcengineering/tracker'
|
||||
import { Issue, IssueStatus, IssueTemplate, Sprint } from '@hcengineering/tracker'
|
||||
import { ButtonKind, ButtonShape, ButtonSize, Label, tooltip } from '@hcengineering/ui'
|
||||
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
|
||||
import { activeSprint } from '../../issues'
|
||||
@ -25,7 +25,7 @@
|
||||
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
|
||||
import SprintSelector from './SprintSelector.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let value: Issue | IssueTemplate
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let popupPlaceholder: IntlString = tracker.string.MoveToSprint
|
||||
@ -47,15 +47,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateCollection(
|
||||
value._class,
|
||||
value.space,
|
||||
value._id,
|
||||
value.attachedTo,
|
||||
value.attachedToClass,
|
||||
value.collection,
|
||||
{ sprint: newSprintId }
|
||||
)
|
||||
await client.update(value, { sprint: newSprintId })
|
||||
}
|
||||
|
||||
$: ids = new Set(issues?.map((it) => it._id) ?? [])
|
||||
|
@ -0,0 +1,230 @@
|
||||
<!--
|
||||
// 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 { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { Data, generateId, Ref } from '@hcengineering/core'
|
||||
import { Card, getClient, KeyedAttribute, SpaceSelector } from '@hcengineering/presentation'
|
||||
import tags, { TagElement } from '@hcengineering/tags'
|
||||
import { IssuePriority, IssueTemplate, Project, Sprint, Team } from '@hcengineering/tracker'
|
||||
import { Button, Component, EditBox, IconAttachment, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { activeProject, activeSprint } from '../../issues'
|
||||
import tracker from '../../plugin'
|
||||
import AssigneeEditor from '../issues/AssigneeEditor.svelte'
|
||||
import PriorityEditor from '../issues/PriorityEditor.svelte'
|
||||
import ProjectSelector from '../ProjectSelector.svelte'
|
||||
import SprintSelector from '../sprints/SprintSelector.svelte'
|
||||
import SubIssueTemplates from './IssueTemplateChilds.svelte'
|
||||
|
||||
export let space: Ref<Team>
|
||||
export let priority: IssuePriority = IssuePriority.NoPriority
|
||||
export let assignee: Ref<Employee> | null = null
|
||||
export let project: Ref<Project> | null = $activeProject ?? null
|
||||
export let sprint: Ref<Sprint> | null = $activeSprint ?? null
|
||||
|
||||
const dueDate: number | undefined = undefined
|
||||
let labels: TagElement[] = []
|
||||
|
||||
let objectId: Ref<IssueTemplate> = generateId()
|
||||
let object: Data<IssueTemplate> = {
|
||||
title: '',
|
||||
description: '',
|
||||
assignee: assignee,
|
||||
project: project,
|
||||
sprint: sprint,
|
||||
priority: priority,
|
||||
dueDate: dueDate ?? null,
|
||||
estimation: 0,
|
||||
children: [],
|
||||
labels: [],
|
||||
comments: 0,
|
||||
attachments: 0
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
let descriptionBox: AttachmentStyledBox
|
||||
|
||||
const key: KeyedAttribute = {
|
||||
key: 'labels',
|
||||
attr: client.getHierarchy().getAttribute(tracker.class.Issue, 'labels')
|
||||
}
|
||||
|
||||
$: _space = space
|
||||
let spaceRef: Team | undefined
|
||||
|
||||
$: canSave = getTitle(object.title ?? '').length > 0
|
||||
|
||||
function getTitle (value: string) {
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
export function canClose (): boolean {
|
||||
return !canSave
|
||||
}
|
||||
|
||||
async function createIssueTemplate () {
|
||||
if (!canSave) {
|
||||
return
|
||||
}
|
||||
|
||||
const value: Data<IssueTemplate> = {
|
||||
title: getTitle(object.title),
|
||||
description: object.description,
|
||||
assignee: object.assignee,
|
||||
project: object.project,
|
||||
sprint: object.sprint,
|
||||
priority: object.priority,
|
||||
dueDate: dueDate ?? null,
|
||||
estimation: object.estimation,
|
||||
children: object.children,
|
||||
comments: 0,
|
||||
attachments: 0,
|
||||
labels: labels.map((it) => it._id)
|
||||
}
|
||||
|
||||
await client.createDoc(tracker.class.IssueTemplate, _space, value, objectId)
|
||||
|
||||
await descriptionBox.createAttachments()
|
||||
objectId = generateId()
|
||||
}
|
||||
|
||||
const handleProjectIdChanged = (projectId: Ref<Project> | null | undefined) => {
|
||||
if (projectId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
object = { ...object, project: projectId }
|
||||
}
|
||||
|
||||
const handleSprintIdChanged = (sprintId: Ref<Sprint> | null | undefined) => {
|
||||
if (sprintId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
object = { ...object, sprint: sprintId }
|
||||
}
|
||||
|
||||
function addTagRef (tag: TagElement): void {
|
||||
labels = [...labels, tag]
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={tracker.string.NewProcess}
|
||||
okAction={createIssueTemplate}
|
||||
{canSave}
|
||||
okLabel={tracker.string.SaveProcess}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
createMore={false}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="flex-row-center">
|
||||
<SpaceSelector
|
||||
_class={tracker.class.Team}
|
||||
label={tracker.string.Team}
|
||||
bind:space={_space}
|
||||
on:space={(evt) => {
|
||||
spaceRef = evt.detail
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title" let:label>
|
||||
<div class="flex-row-center gap-1">
|
||||
<div class="mr-2">
|
||||
<Label {label} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<EditBox
|
||||
bind:value={object.title}
|
||||
placeholder={tracker.string.IssueTitlePlaceholder}
|
||||
maxWidth={'37.5rem'}
|
||||
kind={'large-style'}
|
||||
focus
|
||||
/>
|
||||
<AttachmentStyledBox
|
||||
bind:this={descriptionBox}
|
||||
{objectId}
|
||||
_class={tracker.class.Issue}
|
||||
space={_space}
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
maxHeight={'card'}
|
||||
bind:content={object.description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
<SubIssueTemplates
|
||||
bind:children={object.children}
|
||||
project={object.project}
|
||||
sprint={object.sprint}
|
||||
teamId={spaceRef?.identifier ?? 'TSK'}
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
<div class="flex flex-wrap" style:gap={'0.2vw'}>
|
||||
<PriorityEditor
|
||||
value={object}
|
||||
shouldShowLabel
|
||||
isEditable
|
||||
kind="no-border"
|
||||
size="small"
|
||||
justify="center"
|
||||
on:change={({ detail }) => (object.priority = detail)}
|
||||
/>
|
||||
<AssigneeEditor
|
||||
value={object}
|
||||
size="small"
|
||||
kind="no-border"
|
||||
width={'min-content'}
|
||||
on:change={({ detail }) => (object.assignee = detail)}
|
||||
/>
|
||||
<Component
|
||||
is={tags.component.TagsDropdownEditor}
|
||||
props={{
|
||||
items: labels,
|
||||
key,
|
||||
targetClass: tracker.class.Issue,
|
||||
countLabel: tracker.string.NumberLabels
|
||||
}}
|
||||
on:open={(evt) => {
|
||||
addTagRef(evt.detail)
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
labels = labels.filter((it) => it._id !== evt.detail)
|
||||
}}
|
||||
/>
|
||||
<!-- <EstimationEditor kind={'no-border'} size={'small'} value={object} /> -->
|
||||
<!-- <NumberEditor kind={'no-border'} bind:value={dueDate} placeholder={tracker.string.DueDate}
|
||||
focus={false} onChange={({ detail }) => (dueDate = detail) }/> -->
|
||||
<ProjectSelector value={object.project} onChange={handleProjectIdChanged} />
|
||||
<SprintSelector value={object.sprint} onChange={handleSprintIdChanged} useProject={object.project ?? undefined} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
icon={IconAttachment}
|
||||
kind={'transparent'}
|
||||
on:click={() => {
|
||||
descriptionBox.attach()
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Card>
|
@ -0,0 +1,340 @@
|
||||
<!--
|
||||
// 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 { AttachmentDocList, AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||
import { Class, Data, Doc, Ref, 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 setting, { settingId } from '@hcengineering/setting'
|
||||
import type { IssueTemplate, IssueTemplateChild, Team } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
EditBox,
|
||||
getCurrentLocation,
|
||||
IconAttachment,
|
||||
IconEdit,
|
||||
IconMoreH,
|
||||
Label,
|
||||
navigate,
|
||||
Scroller,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { ContextMenu, UpDownNavigator } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
import SubIssueTemplates from './IssueTemplateChilds.svelte'
|
||||
import TemplateControlPanel from './TemplateControlPanel.svelte'
|
||||
|
||||
export let _id: Ref<IssueTemplate>
|
||||
export let _class: Ref<Class<IssueTemplate>>
|
||||
|
||||
let lastId: Ref<Doc> = _id
|
||||
let lastClass: Ref<Class<Doc>> = _class
|
||||
const query = createQuery()
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
let template: WithLookup<IssueTemplate> | undefined
|
||||
let currentTeam: Team | undefined
|
||||
let title = ''
|
||||
let description = ''
|
||||
let innerWidth: number
|
||||
let isEditing = false
|
||||
let descriptionBox: AttachmentStyledBox
|
||||
|
||||
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
|
||||
|
||||
$: read(_id)
|
||||
function read (_id: Ref<Doc>) {
|
||||
if (lastId !== _id) {
|
||||
const prev = lastId
|
||||
const prevClass = lastClass
|
||||
lastId = _id
|
||||
lastClass = _class
|
||||
notificationClient.then((client) => client.updateLastView(prev, prevClass))
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(async () => {
|
||||
notificationClient.then((client) => client.updateLastView(_id, _class))
|
||||
})
|
||||
|
||||
$: _id &&
|
||||
_class &&
|
||||
query.query(
|
||||
_class,
|
||||
{ _id },
|
||||
async (result) => {
|
||||
;[template] = result
|
||||
title = template.title
|
||||
description = template.description
|
||||
currentTeam = template.$lookup?.space
|
||||
},
|
||||
{ lookup: { space: tracker.class.Team } }
|
||||
)
|
||||
|
||||
$: canSave = title.trim().length > 0
|
||||
$: isDescriptionEmpty = !new DOMParser().parseFromString(description, 'text/html').documentElement.innerText?.trim()
|
||||
|
||||
function edit (ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
|
||||
isEditing = true
|
||||
}
|
||||
|
||||
function cancelEditing (ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
|
||||
isEditing = false
|
||||
|
||||
if (template) {
|
||||
title = template.title
|
||||
description = template.description
|
||||
}
|
||||
}
|
||||
|
||||
async function save (ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
|
||||
if (!template || !canSave) {
|
||||
return
|
||||
}
|
||||
|
||||
const updates: Partial<Data<IssueTemplate>> = {}
|
||||
const trimmedTitle = title.trim()
|
||||
|
||||
if (trimmedTitle.length > 0 && trimmedTitle !== template.title) {
|
||||
updates.title = trimmedTitle
|
||||
}
|
||||
|
||||
if (description !== template.description) {
|
||||
updates.description = description
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await client.update(template, updates)
|
||||
}
|
||||
await descriptionBox.createAttachments()
|
||||
isEditing = false
|
||||
}
|
||||
|
||||
function showMenu (ev?: Event): void {
|
||||
if (template) {
|
||||
showPopup(ContextMenu, { object: template }, (ev as MouseEvent).target as HTMLElement)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', { ignoreKeys: ['comments', 'name', 'description', 'number'] })
|
||||
})
|
||||
const createIssue = async (evt: CustomEvent<IssueTemplateChild>): Promise<void> => {
|
||||
if (template === undefined) {
|
||||
return
|
||||
}
|
||||
await client.update(template, {
|
||||
$push: { children: evt.detail }
|
||||
})
|
||||
}
|
||||
const updateIssue = async (evt: CustomEvent<Partial<IssueTemplateChild>>): Promise<void> => {
|
||||
if (template === undefined) {
|
||||
return
|
||||
}
|
||||
await client.update(template, {
|
||||
$update: {
|
||||
children: {
|
||||
$query: { id: evt.detail.id },
|
||||
$update: evt.detail
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const updateIssues = async (evt: CustomEvent<IssueTemplateChild[]>): Promise<void> => {
|
||||
if (template === undefined) {
|
||||
return
|
||||
}
|
||||
await client.update(template, {
|
||||
children: evt.detail
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if template !== undefined}
|
||||
<Panel
|
||||
object={template}
|
||||
isHeader
|
||||
isAside={true}
|
||||
isSub={false}
|
||||
withoutActivity={isEditing}
|
||||
bind:innerWidth
|
||||
on:close={() => dispatch('close')}
|
||||
>
|
||||
<svelte:fragment slot="navigator">
|
||||
<UpDownNavigator element={template} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
<span class="fs-title">
|
||||
{template.title}
|
||||
</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}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isEditing}
|
||||
<Scroller>
|
||||
<div class="popupPanel-body__main-content py-10 clear-mins content">
|
||||
<EditBox
|
||||
bind:value={title}
|
||||
maxWidth="53.75rem"
|
||||
placeholder={tracker.string.IssueTitlePlaceholder}
|
||||
kind="large-style"
|
||||
/>
|
||||
<div class="flex-between mt-6">
|
||||
{#key description}
|
||||
<div class="flex-grow">
|
||||
<AttachmentStyledBox
|
||||
bind:this={descriptionBox}
|
||||
objectId={_id}
|
||||
_class={tracker.class.Issue}
|
||||
space={template.space}
|
||||
alwaysEdit
|
||||
showButtons
|
||||
maxHeight={'card'}
|
||||
bind:content={description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
<div
|
||||
class="tool"
|
||||
on:click={() => {
|
||||
descriptionBox.attach()
|
||||
}}
|
||||
>
|
||||
<IconAttachment size={'large'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
{:else}
|
||||
<span class="title select-text">{title}</span>
|
||||
<div class="mt-6 description-preview select-text">
|
||||
{#if isDescriptionEmpty}
|
||||
<div class="placeholder" on:click={edit}>
|
||||
<Label label={tracker.string.IssueDescriptionPlaceholder} />
|
||||
</div>
|
||||
{:else}
|
||||
<MessageViewer message={description} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
{#key template._id && currentTeam !== undefined}
|
||||
{#if currentTeam !== undefined}
|
||||
<SubIssueTemplates
|
||||
bind:children={template.children}
|
||||
on:create-issue={createIssue}
|
||||
on:update-issue={updateIssue}
|
||||
on:update-issues={updateIssues}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<AttachmentDocList value={template} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span slot="actions-label"> ID </span>
|
||||
<svelte:fragment slot="actions">
|
||||
<div class="flex-grow" />
|
||||
<!-- {#if issueId}
|
||||
<CopyToClipboard issueUrl={generateIssueShortLink(issueId)} {issueId} />
|
||||
{/if} -->
|
||||
<Button
|
||||
icon={setting.icon.Setting}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: setting.string.ClassSetting }}
|
||||
on:click={(ev) => {
|
||||
ev.stopPropagation()
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = settingId
|
||||
loc.path[3] = 'setting'
|
||||
loc.path[4] = 'classes'
|
||||
loc.path.length = 5
|
||||
loc.query = { _class }
|
||||
loc.fragment = undefined
|
||||
navigate(loc)
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="custom-attributes">
|
||||
{#if template && currentTeam}
|
||||
<TemplateControlPanel issue={template} />
|
||||
{/if}
|
||||
|
||||
<div class="divider" />
|
||||
<!-- <IssueStatusActivity issue={template} /> -->
|
||||
</svelte:fragment>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 1.125rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.description-preview {
|
||||
color: var(--theme-content-color);
|
||||
line-height: 150%;
|
||||
|
||||
.placeholder {
|
||||
color: var(--theme-content-trans-color);
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
grid-column: 1 / 3;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.tool {
|
||||
align-self: start;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.3;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,183 @@
|
||||
<!--
|
||||
// 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 { generateId, Ref } from '@hcengineering/core'
|
||||
import presentation, { getClient, KeyedAttribute } from '@hcengineering/presentation'
|
||||
import tags, { TagElement, TagReference } from '@hcengineering/tags'
|
||||
import { StyledTextArea } from '@hcengineering/text-editor'
|
||||
import { IssuePriority, IssueTemplateChild, Project, Sprint } from '@hcengineering/tracker'
|
||||
import { Button, Component, EditBox } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import AssigneeEditor from '../issues/AssigneeEditor.svelte'
|
||||
import PriorityEditor from '../issues/PriorityEditor.svelte'
|
||||
|
||||
export let sprint: Ref<Sprint> | null = null
|
||||
export let project: Ref<Project> | null = null
|
||||
export let childIssue: IssueTemplateChild | undefined = undefined
|
||||
export let showBorder = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
let newIssue: IssueTemplateChild = childIssue !== undefined ? { ...childIssue } : getIssueDefaults()
|
||||
let thisRef: HTMLDivElement
|
||||
let focusIssueTitle: () => void
|
||||
let labels: TagElement[] = []
|
||||
|
||||
const key: KeyedAttribute = {
|
||||
key: 'labels',
|
||||
attr: client.getHierarchy().getAttribute(tracker.class.IssueTemplate, 'labels')
|
||||
}
|
||||
|
||||
function getIssueDefaults (): IssueTemplateChild {
|
||||
return {
|
||||
id: generateId(),
|
||||
title: '',
|
||||
description: '',
|
||||
assignee: null,
|
||||
project: null,
|
||||
priority: IssuePriority.NoPriority,
|
||||
dueDate: null,
|
||||
sprint: sprint,
|
||||
estimation: 0
|
||||
}
|
||||
}
|
||||
|
||||
function resetToDefaults () {
|
||||
newIssue = getIssueDefaults()
|
||||
focusIssueTitle?.()
|
||||
}
|
||||
|
||||
function getTitle (value: string) {
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function close () {
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
async function createIssue () {
|
||||
if (!canSave) {
|
||||
return
|
||||
}
|
||||
|
||||
const value: IssueTemplateChild = {
|
||||
...newIssue,
|
||||
title: getTitle(newIssue.title),
|
||||
project: project ?? null,
|
||||
labels: labels.map((it) => it._id)
|
||||
}
|
||||
if (childIssue === undefined) {
|
||||
dispatch('create', value)
|
||||
} else {
|
||||
dispatch('close', value)
|
||||
}
|
||||
|
||||
resetToDefaults()
|
||||
}
|
||||
|
||||
function addTagRef (tag: TagElement): void {
|
||||
labels = [...labels, tag]
|
||||
}
|
||||
|
||||
$: thisRef && thisRef.scrollIntoView({ behavior: 'smooth' })
|
||||
$: canSave = getTitle(newIssue.title ?? '').length > 0
|
||||
|
||||
$: labelRefs = labels.map((it) => ({ ...(it as unknown as TagReference), _id: generateId(), tag: it._id }))
|
||||
</script>
|
||||
|
||||
<div bind:this={thisRef} class="flex-col root" class:antiPopup={showBorder}>
|
||||
<div class="flex-row-top">
|
||||
<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">
|
||||
{#key newIssue.description}
|
||||
<StyledTextArea
|
||||
bind:content={newIssue.description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
showButtons={false}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex-between">
|
||||
<div class="buttons-group xsmall-gap">
|
||||
<PriorityEditor
|
||||
value={newIssue}
|
||||
shouldShowLabel
|
||||
isEditable
|
||||
kind="no-border"
|
||||
size="small"
|
||||
justify="center"
|
||||
on:change={({ detail }) => (newIssue.priority = detail)}
|
||||
/>
|
||||
{#key newIssue.assignee}
|
||||
<AssigneeEditor
|
||||
value={newIssue}
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:change={({ detail }) => (newIssue.assignee = detail)}
|
||||
/>
|
||||
{/key}
|
||||
<Component
|
||||
is={tags.component.TagsDropdownEditor}
|
||||
props={{
|
||||
items: labelRefs,
|
||||
key,
|
||||
targetClass: tracker.class.Issue,
|
||||
countLabel: tracker.string.NumberLabels
|
||||
}}
|
||||
on:open={(evt) => {
|
||||
addTagRef(evt.detail)
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
labels = labels.filter((it) => it._id !== evt.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>
|
@ -0,0 +1,244 @@
|
||||
<!--
|
||||
// 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 { Ref } from '@hcengineering/core'
|
||||
import { IssueTemplateChild, Project, Sprint } from '@hcengineering/tracker'
|
||||
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import { ActionContext, FixedColumn } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import Circles from '../icons/Circles.svelte'
|
||||
import AssigneeEditor from '../issues/AssigneeEditor.svelte'
|
||||
import PriorityEditor from '../issues/PriorityEditor.svelte'
|
||||
import IssueTemplateChildEditor from './IssueTemplateChildEditor.svelte'
|
||||
|
||||
export let issues: IssueTemplateChild[]
|
||||
export let sprint: Ref<Sprint> | null = null
|
||||
export let project: Ref<Project> | null = null
|
||||
// export let template: IssueTemplate | Data<IssueTemplate>
|
||||
|
||||
export let teamId: string
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let draggingIndex: number | null = null
|
||||
let hoveringIndex: number | null = null
|
||||
|
||||
function openIssue (evt: MouseEvent, target: IssueTemplateChild) {
|
||||
showPopup(
|
||||
IssueTemplateChildEditor,
|
||||
{
|
||||
showBorder: true,
|
||||
sprint,
|
||||
project,
|
||||
childIssue: target
|
||||
},
|
||||
eventToHTMLElement(evt),
|
||||
(evt: IssueTemplateChild | undefined | null) => {
|
||||
if (evt != null) {
|
||||
const pos = issues.findIndex((it) => it.id === evt.id)
|
||||
if (pos !== -1) {
|
||||
issues[pos] = evt
|
||||
dispatch('update-issue', evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function resetDrag () {
|
||||
draggingIndex = null
|
||||
hoveringIndex = null
|
||||
}
|
||||
|
||||
function handleDragStart (ev: DragEvent, index: number) {
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
draggingIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop (ev: DragEvent, toIndex: number) {
|
||||
if (ev.dataTransfer && draggingIndex !== null && toIndex !== draggingIndex) {
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
|
||||
dispatch('move', { fromIndex: draggingIndex, toIndex })
|
||||
}
|
||||
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
function showContextMenu (ev: MouseEvent, object: IssueTemplateChild) {
|
||||
// showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
let varsStyle: string = ''
|
||||
const propsWidth: Record<string, number> = { issue: 0 }
|
||||
$: if (propsWidth) {
|
||||
varsStyle = ''
|
||||
for (const key in propsWidth) varsStyle += `--fixed-${key}: ${propsWidth[key]}px;`
|
||||
}
|
||||
const checkWidth = (key: string, result: CustomEvent): void => {
|
||||
if (result !== undefined) propsWidth[key] = result.detail
|
||||
}
|
||||
|
||||
export function getIssueTemplateId (team: string, issue: IssueTemplateChild): string {
|
||||
return `${team}-${issues.findIndex((it) => it.id === issue.id)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
context={{
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
|
||||
{#each issues as issue, index (issue.id)}
|
||||
<div
|
||||
class="flex-between row"
|
||||
class:is-dragging={index === draggingIndex}
|
||||
class:is-dragged-over-up={draggingIndex !== null && index < draggingIndex && index === hoveringIndex}
|
||||
class:is-dragged-over-down={draggingIndex !== null && index > draggingIndex && index === hoveringIndex}
|
||||
style={varsStyle}
|
||||
animate:flip={{ duration: 400 }}
|
||||
draggable={true}
|
||||
on:click|self={(evt) => openIssue(evt, issue)}
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
|
||||
on:dragstart={(ev) => handleDragStart(ev, index)}
|
||||
on:dragover|preventDefault={() => false}
|
||||
on:dragenter={() => (hoveringIndex = index)}
|
||||
on:drop|preventDefault={(ev) => handleDrop(ev, index)}
|
||||
on:dragend={resetDrag}
|
||||
>
|
||||
<div class="draggable-container">
|
||||
<div class="draggable-mark"><Circles /></div>
|
||||
</div>
|
||||
<div class="flex-row-center ml-6 clear-mins gap-2">
|
||||
<PriorityEditor
|
||||
value={issue}
|
||||
isEditable
|
||||
kind={'list'}
|
||||
size={'small'}
|
||||
justify={'center'}
|
||||
on:change={(evt) => {
|
||||
dispatch('update-issue', { id: issue.id, priority: evt.detail })
|
||||
issue.priority = evt.detail
|
||||
}}
|
||||
/>
|
||||
<span class="issuePresenter" on:click={(evt) => openIssue(evt, issue)}>
|
||||
<FixedColumn
|
||||
width={propsWidth.issue}
|
||||
key={'issue'}
|
||||
justify={'left'}
|
||||
on:update={(result) => checkWidth('issue', result)}
|
||||
>
|
||||
{getIssueTemplateId(teamId, issue)}
|
||||
</FixedColumn>
|
||||
</span>
|
||||
<span class="text name" title={issue.title} on:click={(evt) => openIssue(evt, issue)}>
|
||||
{issue.title}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-center flex-no-shrink">
|
||||
<AssigneeEditor
|
||||
value={issue}
|
||||
on:change={(evt) => {
|
||||
dispatch('update-issue', { id: issue.id, assignee: evt.detail })
|
||||
issue.assignee = evt.detail
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
.text {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
font-weight: 500;
|
||||
color: var(--content-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:active {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.draggable-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 1.5rem;
|
||||
cursor: grabbing;
|
||||
|
||||
.draggable-mark {
|
||||
opacity: 0;
|
||||
width: 0.375rem;
|
||||
height: 1rem;
|
||||
margin-left: 0.75rem;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.draggable-mark {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background-color: var(--theme-bg-color);
|
||||
opacity: 0.4;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
&.is-dragged-over-up::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-top: 1px solid var(--theme-bg-check);
|
||||
}
|
||||
&.is-dragged-over-down::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-bottom: 1px solid var(--theme-bg-check);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,128 @@
|
||||
<!--
|
||||
// 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 { Ref } from '@hcengineering/core'
|
||||
import { IssueTemplateChild, Project, Sprint } from '@hcengineering/tracker'
|
||||
import { Button, closeTooltip, ExpandCollapse, IconAdd } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import Collapsed from '../icons/Collapsed.svelte'
|
||||
import Expanded from '../icons/Expanded.svelte'
|
||||
import IssueTemplateChildEditor from './IssueTemplateChildEditor.svelte'
|
||||
import IssueTemplateChildList from './IssueTemplateChildList.svelte'
|
||||
|
||||
export let children: IssueTemplateChild[] = []
|
||||
export let teamId: string = 'TSK'
|
||||
export let sprint: Ref<Sprint> | null = null
|
||||
export let project: Ref<Project> | null = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let isCollapsed = false
|
||||
let isCreating = false
|
||||
|
||||
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
|
||||
if (children) {
|
||||
const { fromIndex, toIndex } = ev.detail
|
||||
const [fromIssue] = children.splice(fromIndex, 1)
|
||||
const leftPart = children.slice(0, toIndex)
|
||||
const rightPart = children.slice(toIndex)
|
||||
children = [...leftPart, fromIssue, ...rightPart]
|
||||
dispatch('update-issues', children)
|
||||
}
|
||||
}
|
||||
|
||||
$: hasSubIssues = children.length > 0
|
||||
</script>
|
||||
|
||||
<div class="flex-between">
|
||||
{#if hasSubIssues}
|
||||
<Button
|
||||
width="min-content"
|
||||
icon={isCollapsed ? Collapsed : Expanded}
|
||||
size="small"
|
||||
kind="transparent"
|
||||
label={tracker.string.SubIssuesList}
|
||||
labelParams={{ subIssues: children.length }}
|
||||
on:click={() => {
|
||||
isCollapsed = !isCollapsed
|
||||
isCreating = false
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={hasSubIssues ? IconAdd : undefined}
|
||||
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
isCreating = true
|
||||
isCollapsed = false
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
{#if hasSubIssues}
|
||||
<div class="list" class:collapsed={isCollapsed}>
|
||||
<IssueTemplateChildList
|
||||
{project}
|
||||
{sprint}
|
||||
bind:issues={children}
|
||||
{teamId}
|
||||
on:move={handleIssueSwap}
|
||||
on:update-issue
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
{#if isCreating}
|
||||
<div class="pt-4">
|
||||
<IssueTemplateChildEditor
|
||||
{project}
|
||||
{sprint}
|
||||
on:close={() => {
|
||||
isCreating = false
|
||||
}}
|
||||
on:create={(evt) => {
|
||||
if (children === undefined) {
|
||||
children = []
|
||||
}
|
||||
children = [...children, evt.detail]
|
||||
dispatch('create-issue', evt.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.list {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
||||
&.collapsed {
|
||||
padding-top: 1px;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,67 @@
|
||||
<!--
|
||||
// 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 { WithLookup } from '@hcengineering/core'
|
||||
import type { IssueTemplate } from '@hcengineering/tracker'
|
||||
import { Icon, showPanel } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: WithLookup<IssueTemplate>
|
||||
// export let inline: boolean = false
|
||||
export let disableClick = false
|
||||
|
||||
function handleIssueEditorOpened () {
|
||||
if (disableClick) {
|
||||
return
|
||||
}
|
||||
showPanel(tracker.component.EditIssueTemplate, value._id, value._class, 'content')
|
||||
}
|
||||
|
||||
$: title = `${value?.title}`
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<span class="issuePresenterRoot flex" class:noPointer={disableClick} title="title" on:click={handleIssueEditorOpened}>
|
||||
<Icon icon={tracker.icon.Issues} size={'small'} />
|
||||
<span class="ml-2">
|
||||
{title}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.issuePresenterRoot {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
flex-shrink: 0;
|
||||
max-width: 15rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--content-color);
|
||||
cursor: pointer;
|
||||
|
||||
&.noPointer {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
}
|
||||
&:active {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,26 @@
|
||||
<!--
|
||||
// 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 { DocumentQuery, Ref } from '@hcengineering/core'
|
||||
import { IssueTemplate, Team } from '@hcengineering/tracker'
|
||||
import tracker from '../../plugin'
|
||||
import IssueTemplatesView from './IssueTemplatesView.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
|
||||
const query: DocumentQuery<IssueTemplate> = { space: currentSpace }
|
||||
</script>
|
||||
|
||||
<IssueTemplatesView {query} title={tracker.string.IssueTemplates} />
|
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { DocumentQuery, WithLookup } from '@hcengineering/core'
|
||||
import { IssueTemplate, ViewOptions } from '@hcengineering/tracker'
|
||||
import { Viewlet } from '@hcengineering/view'
|
||||
import { viewOptionsStore } from '@hcengineering/view-resources'
|
||||
import TemplatesList from './TemplatesList.svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let viewlet: WithLookup<Viewlet>
|
||||
export let query: DocumentQuery<IssueTemplate> = {}
|
||||
|
||||
$: vo = $viewOptionsStore as ViewOptions
|
||||
</script>
|
||||
|
||||
{#if viewlet?.$lookup?.descriptor?.component}
|
||||
<TemplatesList _class={tracker.class.IssueTemplate} config={viewlet.config} {query} viewOptions={vo} />
|
||||
{/if}
|
@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
import { DocumentQuery, WithLookup } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { IssueTemplate } from '@hcengineering/tracker'
|
||||
import { Button, IconAdd, IconDetails, IconDetailsFilled, showPopup } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { FilterBar, getActiveViewletId, ViewOptionModel, ViewOptionsButton } from '@hcengineering/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import { getDefaultViewOptionsTemplatesConfig } from '../../utils'
|
||||
import IssuesHeader from '../issues/IssuesHeader.svelte'
|
||||
import CreateIssueTemplate from './CreateIssueTemplate.svelte'
|
||||
import IssueTemplatesContent from './IssueTemplatesContent.svelte'
|
||||
|
||||
export let query: DocumentQuery<IssueTemplate> = {}
|
||||
export let title: IntlString | undefined = undefined
|
||||
export let label: string = ''
|
||||
export let viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsTemplatesConfig()
|
||||
|
||||
export let panelWidth: number = 0
|
||||
|
||||
let viewlet: WithLookup<Viewlet> | undefined = undefined
|
||||
let search = ''
|
||||
let searchQuery: DocumentQuery<IssueTemplate> = { ...query }
|
||||
function updateSearchQuery (search: string): void {
|
||||
searchQuery = search === '' ? { ...query } : { ...query, $search: search }
|
||||
}
|
||||
$: updateSearchQuery(search)
|
||||
$: if (query) updateSearchQuery(search)
|
||||
let resultQuery: DocumentQuery<IssueTemplate> = { ...searchQuery }
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let viewlets: WithLookup<Viewlet>[] = []
|
||||
|
||||
$: update()
|
||||
|
||||
async function update (): Promise<void> {
|
||||
viewlets = await client.findAll(
|
||||
view.class.Viewlet,
|
||||
{ attachTo: tracker.class.IssueTemplate },
|
||||
{
|
||||
lookup: {
|
||||
descriptor: view.class.ViewletDescriptor
|
||||
}
|
||||
}
|
||||
)
|
||||
const _id = getActiveViewletId()
|
||||
viewlet = viewlets.find((viewlet) => viewlet._id === _id) || viewlets[0]
|
||||
}
|
||||
$: if (!label && title) {
|
||||
translate(title, {}).then((res) => {
|
||||
label = res
|
||||
})
|
||||
}
|
||||
|
||||
let asideFloat: boolean = false
|
||||
let asideShown: boolean = true
|
||||
$: if (panelWidth < 900 && !asideFloat) asideFloat = true
|
||||
$: if (panelWidth >= 900 && asideFloat) {
|
||||
asideFloat = false
|
||||
asideShown = false
|
||||
}
|
||||
let docWidth: number
|
||||
let docSize: boolean = false
|
||||
$: if (docWidth <= 900 && !docSize) docSize = true
|
||||
$: if (docWidth > 900 && docSize) docSize = false
|
||||
|
||||
const showCreateDialog = async () => {
|
||||
showPopup(CreateIssueTemplate, { targetElement: null }, null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
|
||||
<svelte:fragment slot="label_selector">
|
||||
<slot name="label_selector" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="extra">
|
||||
<Button
|
||||
size="small"
|
||||
icon={IconAdd}
|
||||
label={tracker.string.IssueTemplate}
|
||||
kind="secondary"
|
||||
on:click={showCreateDialog}
|
||||
/>
|
||||
|
||||
{#if viewlet}
|
||||
<ViewOptionsButton viewOptionsKey={viewlet._id} config={viewOptionsConfig} />
|
||||
{/if}
|
||||
|
||||
{#if asideFloat && $$slots.aside}
|
||||
<div class="buttons-divider" />
|
||||
<Button
|
||||
icon={asideShown ? IconDetailsFilled : IconDetails}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
selected={asideShown}
|
||||
on:click={() => {
|
||||
asideShown = !asideShown
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</IssuesHeader>
|
||||
<slot name="afterHeader" />
|
||||
<FilterBar _class={tracker.class.IssueTemplate} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
|
||||
<div class="flex w-full h-full clear-mins">
|
||||
{#if viewlet}
|
||||
<IssueTemplatesContent {viewlet} query={resultQuery} />
|
||||
{/if}
|
||||
{#if $$slots.aside !== undefined && asideShown}
|
||||
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
@ -0,0 +1,110 @@
|
||||
<!--
|
||||
// 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 { Doc } from '@hcengineering/core'
|
||||
import { AttributeBarEditor, createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation'
|
||||
import type { IssueTemplate } from '@hcengineering/tracker'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { getFiltredKeys, isCollectionAttr } from '@hcengineering/view-resources/src/utils'
|
||||
import tracker from '../../plugin'
|
||||
import AssigneeEditor from '../issues/AssigneeEditor.svelte'
|
||||
import PriorityEditor from '../issues/PriorityEditor.svelte'
|
||||
import ProjectEditor from '../projects/ProjectEditor.svelte'
|
||||
import SprintEditor from '../sprints/SprintEditor.svelte'
|
||||
|
||||
export let issue: IssueTemplate
|
||||
|
||||
const query = createQuery()
|
||||
let showIsBlocking = false
|
||||
let blockedBy: Doc[]
|
||||
$: query.query(tracker.class.Issue, { blockedBy: { _id: issue._id, _class: issue._class } }, (result) => {
|
||||
blockedBy = result
|
||||
showIsBlocking = result.length > 0
|
||||
})
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let keys: KeyedAttribute[] = []
|
||||
|
||||
function updateKeys (ignoreKeys: string[]): void {
|
||||
const filtredKeys = getFiltredKeys(hierarchy, issue._class, ignoreKeys)
|
||||
keys = filtredKeys.filter((key) => !isCollectionAttr(hierarchy, key))
|
||||
}
|
||||
|
||||
$: updateKeys(['title', 'description', 'priority', 'number', 'assignee', 'project', 'sprint'])
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<span class="label">
|
||||
<Label label={tracker.string.Priority} />
|
||||
</span>
|
||||
<PriorityEditor value={issue} shouldShowLabel />
|
||||
|
||||
<span class="label">
|
||||
<Label label={tracker.string.Assignee} />
|
||||
</span>
|
||||
<AssigneeEditor value={issue} />
|
||||
|
||||
<!-- <span class="labelTop">
|
||||
<Label label={tracker.string.Labels} />
|
||||
</span> -->
|
||||
<!-- <Component is={tags.component.TagsAttributeEditor} props={{ object: issue, label: tracker.string.AddLabel }} /> -->
|
||||
|
||||
<div class="divider" />
|
||||
|
||||
<span class="label">
|
||||
<Label label={tracker.string.Project} />
|
||||
</span>
|
||||
<ProjectEditor value={issue} />
|
||||
|
||||
{#if issue.sprint}
|
||||
<span class="label">
|
||||
<Label label={tracker.string.Sprint} />
|
||||
</span>
|
||||
<SprintEditor value={issue} />
|
||||
{/if}
|
||||
|
||||
{#if keys.length > 0}
|
||||
<div class="divider" />
|
||||
{#each keys as key (typeof key === 'string' ? key : key.key)}
|
||||
<AttributeBarEditor {key} _class={issue._class} object={issue} showHeader={true} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr;
|
||||
grid-auto-flow: row;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
.divider {
|
||||
grid-column: 1 / 3;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.labelTop {
|
||||
align-self: start;
|
||||
margin-top: 0.385rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import { Class, Doc, DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { IssueTemplate, ViewOptions } from '@hcengineering/tracker'
|
||||
import { defaultSP, Scroller } from '@hcengineering/ui'
|
||||
import { BuildModelKey } from '@hcengineering/view'
|
||||
import tracker from '../../plugin'
|
||||
import {
|
||||
getCategories,
|
||||
groupBy as groupByFunc,
|
||||
issuesGroupKeyMap,
|
||||
issuesOrderKeyMap,
|
||||
issuesSortOrderMap
|
||||
} from '../../utils'
|
||||
import IssuesListBrowser from '../issues/IssuesListBrowser.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let config: (string | BuildModelKey)[]
|
||||
export let query: DocumentQuery<IssueTemplate> = {}
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
|
||||
$: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions)
|
||||
$: groupByKey = issuesGroupKeyMap[groupBy]
|
||||
$: orderByKey = issuesOrderKeyMap[orderBy]
|
||||
|
||||
$: groupedIssues = groupByFunc(issues, groupBy)
|
||||
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups, [], employees)
|
||||
$: employees = issues.map((x) => x.$lookup?.assignee).filter(Boolean) as Employee[]
|
||||
|
||||
const issuesQuery = createQuery()
|
||||
let issues: WithLookup<IssueTemplate>[] = []
|
||||
$: issuesQuery.query(
|
||||
tracker.class.IssueTemplate,
|
||||
{ ...query },
|
||||
(result) => {
|
||||
issues = result
|
||||
},
|
||||
{
|
||||
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
space: tracker.class.Team,
|
||||
sprint: tracker.class.Sprint
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full clear-mins">
|
||||
<Scroller fade={defaultSP}>
|
||||
<IssuesListBrowser
|
||||
{_class}
|
||||
{currentSpace}
|
||||
{groupByKey}
|
||||
orderBy={orderByKey}
|
||||
statuses={[]}
|
||||
{employees}
|
||||
{categories}
|
||||
itemsConfig={config}
|
||||
{groupedIssues}
|
||||
/>
|
||||
</Scroller>
|
||||
</div>
|
@ -79,6 +79,11 @@ import RelatedIssues from './components/issues/related/RelatedIssues.svelte'
|
||||
|
||||
import ProjectSelector from './components/ProjectSelector.svelte'
|
||||
|
||||
import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte'
|
||||
import IssueTemplates from './components/templates/IssueTemplates.svelte'
|
||||
|
||||
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
|
||||
|
||||
export async function queryIssue<D extends Issue> (
|
||||
_class: Ref<Class<D>>,
|
||||
client: Client,
|
||||
@ -195,7 +200,10 @@ export default async (): Promise<Resources> => ({
|
||||
SubIssuesSelector,
|
||||
GrowPresenter,
|
||||
RelatedIssues,
|
||||
ProjectSelector
|
||||
ProjectSelector,
|
||||
IssueTemplates,
|
||||
IssueTemplatePresenter,
|
||||
EditIssueTemplate
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||
|
@ -117,6 +117,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
ChangeDueDate: '' as IntlString,
|
||||
ModificationDate: '' as IntlString,
|
||||
Issue: '' as IntlString,
|
||||
IssueTemplate: '' as IntlString,
|
||||
Document: '' as IntlString,
|
||||
DocumentIcon: '' as IntlString,
|
||||
DocumentColor: '' as IntlString,
|
||||
@ -232,7 +233,14 @@ export default mergeIds(trackerId, tracker, {
|
||||
CapacityValue: '' as IntlString,
|
||||
AddedReference: '' as IntlString,
|
||||
AddedAsBlocked: '' as IntlString,
|
||||
AddedAsBlocking: '' as IntlString
|
||||
AddedAsBlocking: '' as IntlString,
|
||||
|
||||
IssueTemplates: '' as IntlString,
|
||||
NewProcess: '' as IntlString,
|
||||
SaveProcess: '' as IntlString,
|
||||
NoIssueTemplate: '' as IntlString,
|
||||
TemplateReplace: '' as IntlString,
|
||||
TemplateReplaceConfirm: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
NopeComponent: '' as AnyComponent,
|
||||
@ -257,6 +265,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
AssigneePresenter: '' as AnyComponent,
|
||||
DueDatePresenter: '' as AnyComponent,
|
||||
EditIssue: '' as AnyComponent,
|
||||
EditIssueTemplate: '' as AnyComponent,
|
||||
CreateTeam: '' as AnyComponent,
|
||||
NewIssueHeader: '' as AnyComponent,
|
||||
IconPresenter: '' as AnyComponent,
|
||||
@ -286,7 +295,10 @@ export default mergeIds(trackerId, tracker, {
|
||||
EstimationEditor: '' as AnyComponent,
|
||||
GrowPresenter: '' as AnyComponent,
|
||||
|
||||
ProjectSelector: '' as AnyComponent
|
||||
ProjectSelector: '' as AnyComponent,
|
||||
|
||||
IssueTemplates: '' as AnyComponent,
|
||||
IssueTemplatePresenter: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
IssueStatus,
|
||||
IssueTemplate,
|
||||
ProjectStatus,
|
||||
Sprint,
|
||||
SprintStatus,
|
||||
@ -305,7 +306,7 @@ const listIssueStatusOrder = [
|
||||
|
||||
export function getCategories (
|
||||
key: IssuesGroupByKeys | undefined,
|
||||
elements: Array<WithLookup<Issue>>,
|
||||
elements: Array<WithLookup<Issue | IssueTemplate>>,
|
||||
shouldShowAll: boolean,
|
||||
statuses: IssueStatus[],
|
||||
employees: Employee[]
|
||||
@ -320,7 +321,7 @@ export function getCategories (
|
||||
const existingCategories = Array.from(
|
||||
new Set(
|
||||
elements.map((x) => {
|
||||
return x[key]
|
||||
return (x as any)[key]
|
||||
})
|
||||
)
|
||||
)
|
||||
@ -509,49 +510,90 @@ export async function getPriorityStates (): Promise<TypeState[]> {
|
||||
)
|
||||
}
|
||||
|
||||
export function getDefaultViewOptionsConfig (): ViewOptionModel[] {
|
||||
return [
|
||||
{
|
||||
key: 'groupBy',
|
||||
label: tracker.string.Grouping,
|
||||
defaultValue: 'status',
|
||||
values: [
|
||||
{ id: 'status', label: tracker.string.Status },
|
||||
{ id: 'assignee', label: tracker.string.Assignee },
|
||||
{ id: 'priority', label: tracker.string.Priority },
|
||||
{ id: 'project', label: tracker.string.Project },
|
||||
{ id: 'sprint', label: tracker.string.Sprint },
|
||||
{ id: 'noGrouping', label: tracker.string.NoGrouping }
|
||||
],
|
||||
type: 'dropdown'
|
||||
},
|
||||
{
|
||||
key: 'orderBy',
|
||||
label: tracker.string.Ordering,
|
||||
defaultValue: 'status',
|
||||
values: [
|
||||
{ id: 'status', label: tracker.string.Status },
|
||||
{ id: 'modifiedOn', label: tracker.string.LastUpdated },
|
||||
{ id: 'priority', label: tracker.string.Priority },
|
||||
{ id: 'dueDate', label: tracker.string.DueDate },
|
||||
{ id: 'rank', label: tracker.string.Manual }
|
||||
],
|
||||
type: 'dropdown'
|
||||
},
|
||||
{
|
||||
key: 'shouldShowSubIssues',
|
||||
label: tracker.string.SubIssues,
|
||||
defaultValue: false,
|
||||
type: 'toggle'
|
||||
},
|
||||
{
|
||||
key: 'shouldShowEmptyGroups',
|
||||
label: tracker.string.ShowEmptyGroups,
|
||||
defaultValue: false,
|
||||
type: 'toggle',
|
||||
hidden: ({ groupBy }) => !['status', 'priority'].includes(groupBy)
|
||||
}
|
||||
]
|
||||
export function getDefaultViewOptionsConfig (enableSubIssues = true): ViewOptionModel[] {
|
||||
const groupByCategory: ViewOptionModel = {
|
||||
key: 'groupBy',
|
||||
label: tracker.string.Grouping,
|
||||
defaultValue: 'status',
|
||||
values: [
|
||||
{ id: 'status', label: tracker.string.Status },
|
||||
{ id: 'assignee', label: tracker.string.Assignee },
|
||||
{ id: 'priority', label: tracker.string.Priority },
|
||||
{ id: 'project', label: tracker.string.Project },
|
||||
{ id: 'sprint', label: tracker.string.Sprint },
|
||||
{ id: 'noGrouping', label: tracker.string.NoGrouping }
|
||||
],
|
||||
type: 'dropdown'
|
||||
}
|
||||
const orderByCategory: ViewOptionModel = {
|
||||
key: 'orderBy',
|
||||
label: tracker.string.Ordering,
|
||||
defaultValue: 'status',
|
||||
values: [
|
||||
{ id: 'status', label: tracker.string.Status },
|
||||
{ id: 'modifiedOn', label: tracker.string.LastUpdated },
|
||||
{ id: 'priority', label: tracker.string.Priority },
|
||||
{ id: 'dueDate', label: tracker.string.DueDate },
|
||||
{ id: 'rank', label: tracker.string.Manual }
|
||||
],
|
||||
type: 'dropdown'
|
||||
}
|
||||
const showSubIssuesCategory: ViewOptionModel = {
|
||||
key: 'shouldShowSubIssues',
|
||||
label: tracker.string.SubIssues,
|
||||
defaultValue: false,
|
||||
type: 'toggle'
|
||||
}
|
||||
const showEmptyGroups: ViewOptionModel = {
|
||||
key: 'shouldShowEmptyGroups',
|
||||
label: tracker.string.ShowEmptyGroups,
|
||||
defaultValue: false,
|
||||
type: 'toggle',
|
||||
hidden: ({ groupBy }) => !['status', 'priority'].includes(groupBy)
|
||||
}
|
||||
const result: ViewOptionModel[] = [groupByCategory, orderByCategory]
|
||||
if (enableSubIssues) {
|
||||
result.push(showSubIssuesCategory)
|
||||
}
|
||||
result.push(showEmptyGroups)
|
||||
return result
|
||||
}
|
||||
|
||||
export function getDefaultViewOptionsTemplatesConfig (): ViewOptionModel[] {
|
||||
const groupByCategory: ViewOptionModel = {
|
||||
key: 'groupBy',
|
||||
label: tracker.string.Grouping,
|
||||
defaultValue: 'project',
|
||||
values: [
|
||||
{ id: 'assignee', label: tracker.string.Assignee },
|
||||
{ id: 'priority', label: tracker.string.Priority },
|
||||
{ id: 'project', label: tracker.string.Project },
|
||||
{ id: 'sprint', label: tracker.string.Sprint },
|
||||
{ id: 'noGrouping', label: tracker.string.NoGrouping }
|
||||
],
|
||||
type: 'dropdown'
|
||||
}
|
||||
const orderByCategory: ViewOptionModel = {
|
||||
key: 'orderBy',
|
||||
label: tracker.string.Ordering,
|
||||
defaultValue: 'priority',
|
||||
values: [
|
||||
{ id: 'modifiedOn', label: tracker.string.LastUpdated },
|
||||
{ id: 'priority', label: tracker.string.Priority },
|
||||
{ id: 'dueDate', label: tracker.string.DueDate }
|
||||
],
|
||||
type: 'dropdown'
|
||||
}
|
||||
const showEmptyGroups: ViewOptionModel = {
|
||||
key: 'shouldShowEmptyGroups',
|
||||
label: tracker.string.ShowEmptyGroups,
|
||||
defaultValue: false,
|
||||
type: 'toggle',
|
||||
hidden: ({ groupBy }) => !['status', 'priority'].includes(groupBy)
|
||||
}
|
||||
const result: ViewOptionModel[] = [groupByCategory, orderByCategory]
|
||||
result.push(showEmptyGroups)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,7 +17,7 @@ import { Employee } from '@hcengineering/contact'
|
||||
import type { AttachedDoc, Class, Doc, Markup, Ref, RelatedDocument, Space, Timestamp, Type } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { TagCategory } from '@hcengineering/tags'
|
||||
import type { TagCategory, TagElement } from '@hcengineering/tags'
|
||||
import { AnyComponent, Location } from '@hcengineering/ui'
|
||||
import { Action, ActionCategory } from '@hcengineering/view'
|
||||
|
||||
@ -172,6 +172,54 @@ export interface Issue extends AttachedDoc {
|
||||
reports: number
|
||||
|
||||
childInfo: IssueChildInfo[]
|
||||
|
||||
template?: {
|
||||
// A template issue is based on
|
||||
template: Ref<IssueTemplate>
|
||||
// Child id in template
|
||||
childId?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IssueTemplateData {
|
||||
title: string
|
||||
description: Markup
|
||||
priority: IssuePriority
|
||||
|
||||
assignee: Ref<Employee> | null
|
||||
project: Ref<Project> | null
|
||||
|
||||
sprint?: Ref<Sprint> | null
|
||||
|
||||
// Estimation in man days
|
||||
estimation: number
|
||||
|
||||
dueDate: number | null
|
||||
|
||||
labels?: Ref<TagElement>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IssueTemplateChild extends IssueTemplateData {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IssueTemplate extends Doc, IssueTemplateData {
|
||||
space: Ref<Team>
|
||||
|
||||
children: IssueTemplateChild[]
|
||||
|
||||
// Discussion stuff
|
||||
comments: number
|
||||
attachments?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@ -279,6 +327,7 @@ export default plugin(trackerId, {
|
||||
class: {
|
||||
Team: '' as Ref<Class<Team>>,
|
||||
Issue: '' as Ref<Class<Issue>>,
|
||||
IssueTemplate: '' as Ref<Class<IssueTemplate>>,
|
||||
Document: '' as Ref<Class<Document>>,
|
||||
Project: '' as Ref<Class<Project>>,
|
||||
IssueStatus: '' as Ref<Class<IssueStatus>>,
|
||||
|
@ -20,9 +20,10 @@
|
||||
|
||||
// export let label: IntlString
|
||||
export let placeholder: IntlString
|
||||
export let value: number
|
||||
export let value: number | undefined
|
||||
export let focus: boolean
|
||||
export let onChange: (value: number) => void
|
||||
export let maxWidth: string = '10rem'
|
||||
export let onChange: (value: number | undefined) => void
|
||||
export let kind: 'no-border' | 'link' = 'no-border'
|
||||
export let readonly = false
|
||||
|
||||
|
162
plugins/view-resources/src/components/ObjectBox.svelte
Normal file
162
plugins/view-resources/src/components/ObjectBox.svelte
Normal file
@ -0,0 +1,162 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import presentation, { getClient, ObjectCreate } from '@hcengineering/presentation'
|
||||
import {
|
||||
ActionIcon,
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
getEventPositionElement,
|
||||
getFocusManager,
|
||||
Icon,
|
||||
IconOpen,
|
||||
Label,
|
||||
LabelAndProps,
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ObjectBoxPopup from './ObjectBoxPopup.svelte'
|
||||
import ObjectPresenter from './ObjectPresenter.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let excluded: Ref<Doc>[] | undefined = undefined
|
||||
export let options: FindOptions<Doc> | undefined = undefined
|
||||
export let docQuery: DocumentQuery<Doc> | undefined = undefined
|
||||
export let label: IntlString
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let value: Ref<Doc> | null | undefined
|
||||
export let allowDeselect = false
|
||||
export let titleDeselect: IntlString | undefined = undefined
|
||||
export let readonly = false
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let focusIndex = -1
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
export let showNavigate = true
|
||||
export let id: string | undefined = undefined
|
||||
export let searchField: string = 'name'
|
||||
export let docProps: Record<string, any> = {}
|
||||
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let selected: Doc | undefined
|
||||
let container: HTMLElement
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function updateSelected (value: Ref<Doc> | null | undefined) {
|
||||
selected = value ? await client.findOne(_class, { _id: value }) : undefined
|
||||
}
|
||||
|
||||
$: updateSelected(value)
|
||||
|
||||
const mgr = getFocusManager()
|
||||
|
||||
const _click = (ev: MouseEvent): void => {
|
||||
if (!readonly) {
|
||||
showPopup(
|
||||
ObjectBoxPopup,
|
||||
{
|
||||
_class,
|
||||
options,
|
||||
docQuery,
|
||||
ignoreObjects: excluded ?? [],
|
||||
icon,
|
||||
allowDeselect,
|
||||
selected: value,
|
||||
titleDeselect,
|
||||
placeholder,
|
||||
create,
|
||||
searchField,
|
||||
docProps
|
||||
},
|
||||
!$$slots.content ? container : getEventPositionElement(ev),
|
||||
(result) => {
|
||||
if (result === null) {
|
||||
value = null
|
||||
selected = undefined
|
||||
dispatch('change', null)
|
||||
} else if (result !== undefined && result._id !== value) {
|
||||
value = result._id
|
||||
dispatch('change', value)
|
||||
}
|
||||
mgr?.setFocusPos(focusIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$: hideIcon = size === 'x-large' || (size === 'large' && kind !== 'link')
|
||||
</script>
|
||||
|
||||
<div {id} bind:this={container} class="min-w-0" class:w-full={width === '100%'} class:h-full={$$slots.content}>
|
||||
{#if $$slots.content}
|
||||
<div class="w-full h-full flex-streatch" on:click={_click}>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
{:else}
|
||||
<Button {focusIndex} width={width ?? 'min-content'} {size} {kind} {justify} {showTooltip} on:click={_click}>
|
||||
<span slot="content" class="overflow-label flex-grow" class:flex-between={showNavigate && selected}>
|
||||
<div
|
||||
class="disabled"
|
||||
style:width={showNavigate && selected
|
||||
? `calc(${width ?? 'min-content'} - 1.5rem)`
|
||||
: `${width ?? 'min-content'}`}
|
||||
>
|
||||
{#if selected}
|
||||
<ObjectPresenter
|
||||
objectId={selected._id}
|
||||
_class={selected._class}
|
||||
value={selected}
|
||||
props={{ ...docProps, isInteractive: false, inline: true, size: 'x-small' }}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-row-center">
|
||||
{#if icon}
|
||||
<Icon {icon} size={kind === 'link' ? 'small' : size} />
|
||||
{/if}
|
||||
<div class="ml-2">
|
||||
<Label {label} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selected && showNavigate}
|
||||
<ActionIcon
|
||||
icon={IconOpen}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
if (selected) {
|
||||
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
73
plugins/view-resources/src/components/ObjectBoxPopup.svelte
Normal file
73
plugins/view-resources/src/components/ObjectBoxPopup.svelte
Normal file
@ -0,0 +1,73 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { Person } from '@hcengineering/contact'
|
||||
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import presentation, { ObjectCreate, ObjectPopup } from '@hcengineering/presentation'
|
||||
import { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
import ObjectPresenter from './ObjectPresenter.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let options: FindOptions<Doc> = {}
|
||||
export let selected: Ref<Doc> | undefined
|
||||
export let docQuery: DocumentQuery<Doc> = {}
|
||||
|
||||
export let multiSelect: boolean = false
|
||||
export let allowDeselect: boolean = false
|
||||
export let titleDeselect: IntlString | undefined = undefined
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let selectedObjects: Ref<Person>[] = []
|
||||
export let ignoreObjects: Ref<Person>[] = []
|
||||
export let shadows: boolean = true
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
export let searchField: string = 'name'
|
||||
export let docProps: Record<string, any> = {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
afterUpdate(() => dispatch('changeContent'))
|
||||
</script>
|
||||
|
||||
<ObjectPopup
|
||||
{_class}
|
||||
{options}
|
||||
{selected}
|
||||
{searchField}
|
||||
{multiSelect}
|
||||
{allowDeselect}
|
||||
{titleDeselect}
|
||||
{placeholder}
|
||||
{docQuery}
|
||||
groupBy={'_class'}
|
||||
bind:selectedObjects
|
||||
bind:ignoreObjects
|
||||
{shadows}
|
||||
{create}
|
||||
on:update
|
||||
on:close
|
||||
on:changeContent={() => dispatch('changeContent')}
|
||||
>
|
||||
<svelte:fragment slot="item" let:item={doc}>
|
||||
<div class="flex flex-grow overflow-label">
|
||||
<ObjectPresenter
|
||||
objectId={doc._id}
|
||||
_class={doc._class}
|
||||
value={doc}
|
||||
props={{ ...docProps, isInteractive: false, inline: true, size: 'x-small' }}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ObjectPopup>
|
@ -77,6 +77,8 @@ export { default as ContextMenu } from './components/Menu.svelte'
|
||||
export { default as TableBrowser } from './components/TableBrowser.svelte'
|
||||
export { default as FixedColumn } from './components/FixedColumn.svelte'
|
||||
export { default as ViewOptionsButton } from './components/ViewOptionsButton.svelte'
|
||||
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
||||
export { default as ObjectBox } from './components/ObjectBox.svelte'
|
||||
export * from './context'
|
||||
export * from './filter'
|
||||
export * from './selection'
|
||||
|
@ -28,6 +28,7 @@ import core, {
|
||||
Lookup,
|
||||
Mixin,
|
||||
ModelDb,
|
||||
QueryUpdate,
|
||||
Ref,
|
||||
ReverseLookups,
|
||||
SortingOrder,
|
||||
@ -649,6 +650,37 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
}
|
||||
]
|
||||
return await this.db.collection(domain).bulkWrite(ops as any)
|
||||
} else if (operator === '$update') {
|
||||
const keyval = (tx.operations as any).$update
|
||||
const arr = Object.keys(keyval)[0]
|
||||
const desc = keyval[arr] as QueryUpdate<any>
|
||||
const ops = [
|
||||
{
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: tx.objectId,
|
||||
...Object.fromEntries(Object.entries(desc.$query).map((it) => [arr + '.' + it[0], it[1]]))
|
||||
},
|
||||
update: {
|
||||
$set: {
|
||||
...Object.fromEntries(Object.entries(desc.$update).map((it) => [arr + '.$.' + it[0], it[1]]))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
updateOne: {
|
||||
filter: { _id: tx.objectId },
|
||||
update: {
|
||||
$set: {
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
return await this.db.collection(domain).bulkWrite(ops as any)
|
||||
} else {
|
||||
if (tx.retrieve === true) {
|
||||
const result = await this.db.collection(domain).findOneAndUpdate(
|
||||
|
@ -42,7 +42,7 @@ test.describe('recruit tests', () => {
|
||||
await page.locator('.antiPopup').locator('text=Email').click()
|
||||
const emailInput = page.locator('[placeholder="john\\.appleseed@apple\\.com"]')
|
||||
await emailInput.fill(email)
|
||||
await emailInput.press('Enter')
|
||||
await page.locator('#channel-ok.button').click()
|
||||
|
||||
await page.locator('.antiCard button:has-text("Create")').click()
|
||||
await page.waitForSelector('form.antiCard', { state: 'detached' })
|
||||
|
Loading…
Reference in New Issue
Block a user