mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-08 02:37:44 +02:00
UBER-799: Allow extensions to tracker for github (#3727)
This commit is contained in:
@@ -1 +1 @@
|
||||
{ "major": 0, "minor": 6, "patch": 120 }
|
||||
{ "major": 0, "minor": 6, "patch": 130 }
|
||||
|
||||
@@ -18,9 +18,14 @@ import { Builder, Model } from '@hcengineering/model'
|
||||
import core, { TDoc } from '@hcengineering/model-core'
|
||||
import { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
// Import types to prevent .svelte components to being exposed to type typescript.
|
||||
import { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/presentation/src/types'
|
||||
import {
|
||||
ComponentPointExtension,
|
||||
ObjectSearchCategory,
|
||||
ObjectSearchFactory
|
||||
} from '@hcengineering/presentation/src/types'
|
||||
import presentation from './plugin'
|
||||
import { PresentationMiddlewareCreator, PresentationMiddlewareFactory } from '@hcengineering/presentation'
|
||||
import { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
|
||||
|
||||
export { presentationId } from '@hcengineering/presentation/src/plugin'
|
||||
export { default } from './plugin'
|
||||
@@ -40,6 +45,14 @@ export class TPresentationMiddlewareFactory extends TDoc implements Presentation
|
||||
createPresentationMiddleware!: Resource<PresentationMiddlewareCreator>
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TObjectSearchCategory, TPresentationMiddlewareFactory)
|
||||
@Model(presentation.class.ComponentPointExtension, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TComponentPointExtension extends TDoc implements ComponentPointExtension {
|
||||
extension!: ComponentExtensionId
|
||||
component!: AnyComponent
|
||||
props!: Record<string, any>
|
||||
order!: number
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TObjectSearchCategory, TPresentationMiddlewareFactory, TComponentPointExtension)
|
||||
}
|
||||
|
||||
+200
-172
@@ -73,7 +73,7 @@ import {
|
||||
TimeSpendReport,
|
||||
trackerId
|
||||
} from '@hcengineering/tracker'
|
||||
import { KeyBinding, ViewAction, ViewOptionsModel } from '@hcengineering/view'
|
||||
import { BuildModelKey, KeyBinding, ViewAction, ViewOptionsModel } from '@hcengineering/view'
|
||||
import tracker from './plugin'
|
||||
|
||||
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
||||
@@ -425,6 +425,121 @@ export function createModel (builder: Builder): void {
|
||||
TProjectIssueTargetOptions
|
||||
)
|
||||
|
||||
function issueConfig (
|
||||
key: string = '',
|
||||
compact: boolean = false,
|
||||
milestone: boolean = true,
|
||||
component: boolean = true
|
||||
): (BuildModelKey | string)[] {
|
||||
return [
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Priority,
|
||||
presenter: tracker.component.PriorityEditor,
|
||||
props: { type: 'priority', kind: 'list', size: 'small' },
|
||||
displayProps: { key: 'priority' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Identifier,
|
||||
presenter: tracker.component.IssuePresenter,
|
||||
displayProps: { key: key + 'issue', fixed: 'left' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Status,
|
||||
presenter: tracker.component.StatusEditor,
|
||||
props: { kind: 'list', size: 'small', justify: 'center' },
|
||||
displayProps: { key: key + 'status' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Title,
|
||||
presenter: tracker.component.TitlePresenter,
|
||||
props: compact ? { shouldUseMargin: true, showParent: false } : {},
|
||||
displayProps: { key: key + 'title' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.SubIssues,
|
||||
presenter: tracker.component.SubIssuesSelector,
|
||||
props: {}
|
||||
},
|
||||
{ key: 'comments', displayProps: { key: key + 'comments', suffix: true } },
|
||||
{ key: 'attachments', displayProps: { key: key + 'attachments', suffix: true } },
|
||||
{ key: '', displayProps: { grow: true } },
|
||||
{
|
||||
key: 'labels',
|
||||
presenter: tags.component.LabelsPresenter,
|
||||
displayProps: { compression: true },
|
||||
props: { kind: 'list', full: false }
|
||||
},
|
||||
...(milestone
|
||||
? [
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Milestone,
|
||||
presenter: tracker.component.MilestoneEditor,
|
||||
props: {
|
||||
kind: 'list',
|
||||
size: 'small',
|
||||
shouldShowPlaceholder: false
|
||||
},
|
||||
displayProps: {
|
||||
key: key + 'milestone',
|
||||
excludeByKey: 'milestone',
|
||||
compression: true
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(component
|
||||
? [
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Component,
|
||||
presenter: tracker.component.ComponentEditor,
|
||||
props: {
|
||||
kind: 'list',
|
||||
size: 'small',
|
||||
shouldShowPlaceholder: false
|
||||
},
|
||||
displayProps: {
|
||||
key: key + 'component',
|
||||
excludeByKey: 'component',
|
||||
compression: true
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.DueDate,
|
||||
presenter: tracker.component.DueDatePresenter,
|
||||
displayProps: { key: key + 'dueDate', compression: true },
|
||||
props: { kind: 'list' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Estimation,
|
||||
presenter: tracker.component.EstimationEditor,
|
||||
props: { kind: 'list', size: 'small' },
|
||||
displayProps: { key: key + 'estimation', fixed: 'left', dividerBefore: true, optional: true }
|
||||
},
|
||||
{
|
||||
key: 'modifiedOn',
|
||||
presenter: tracker.component.ModificationDatePresenter,
|
||||
displayProps: { key: key + 'modified', fixed: 'left', dividerBefore: true }
|
||||
},
|
||||
{
|
||||
key: 'assignee',
|
||||
presenter: tracker.component.AssigneeEditor,
|
||||
displayProps: { key: 'assignee', fixed: 'right' },
|
||||
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
@@ -453,107 +568,7 @@ export function createModel (builder: Builder): void {
|
||||
'modifiedBy'
|
||||
]
|
||||
},
|
||||
config: [
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Priority,
|
||||
presenter: tracker.component.PriorityEditor,
|
||||
props: { type: 'priority', kind: 'list', size: 'small' },
|
||||
displayProps: { key: 'priority' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Identifier,
|
||||
presenter: tracker.component.IssuePresenter,
|
||||
displayProps: { key: 'issue', fixed: 'left' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Status,
|
||||
presenter: tracker.component.StatusEditor,
|
||||
props: { kind: 'list', size: 'small', justify: 'center' },
|
||||
displayProps: {
|
||||
key: 'status'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Title,
|
||||
presenter: tracker.component.TitlePresenter,
|
||||
props: {},
|
||||
displayProps: { key: 'title' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.SubIssues,
|
||||
presenter: tracker.component.SubIssuesSelector,
|
||||
props: {}
|
||||
},
|
||||
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
|
||||
{ key: 'attachments', displayProps: { key: 'attachments', suffix: true } },
|
||||
{ key: '', displayProps: { grow: true } },
|
||||
{
|
||||
key: 'labels',
|
||||
presenter: tags.component.LabelsPresenter,
|
||||
displayProps: { compression: true },
|
||||
props: { kind: 'list', full: false }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Milestone,
|
||||
presenter: tracker.component.MilestoneEditor,
|
||||
props: {
|
||||
kind: 'list',
|
||||
size: 'small',
|
||||
shouldShowPlaceholder: false
|
||||
},
|
||||
displayProps: {
|
||||
key: 'milestone',
|
||||
excludeByKey: 'milestone',
|
||||
compression: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Component,
|
||||
presenter: tracker.component.ComponentEditor,
|
||||
props: {
|
||||
kind: 'list',
|
||||
size: 'small',
|
||||
shouldShowPlaceholder: false
|
||||
},
|
||||
displayProps: {
|
||||
key: 'component',
|
||||
excludeByKey: 'component',
|
||||
compression: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.DueDate,
|
||||
presenter: tracker.component.DueDatePresenter,
|
||||
displayProps: { key: 'dueDate', compression: true },
|
||||
props: { kind: 'list' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Estimation,
|
||||
presenter: tracker.component.EstimationEditor,
|
||||
props: { kind: 'list', size: 'small' },
|
||||
displayProps: { key: 'estimation', fixed: 'left', dividerBefore: true, optional: true }
|
||||
},
|
||||
{
|
||||
key: 'modifiedOn',
|
||||
presenter: tracker.component.ModificationDatePresenter,
|
||||
displayProps: { key: 'modified', fixed: 'left', dividerBefore: true }
|
||||
},
|
||||
{
|
||||
key: 'assignee',
|
||||
presenter: tracker.component.AssigneeEditor,
|
||||
displayProps: { key: 'assignee', fixed: 'right' },
|
||||
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' }
|
||||
}
|
||||
]
|
||||
config: issueConfig()
|
||||
},
|
||||
tracker.viewlet.IssueList
|
||||
)
|
||||
@@ -594,80 +609,93 @@ export function createModel (builder: Builder): void {
|
||||
'modifiedBy'
|
||||
]
|
||||
},
|
||||
config: [
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Priority,
|
||||
presenter: tracker.component.PriorityEditor,
|
||||
props: { type: 'priority', kind: 'list', size: 'small' },
|
||||
displayProps: { key: 'subpriority' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Issue,
|
||||
presenter: tracker.component.IssuePresenter,
|
||||
props: { type: 'issue' },
|
||||
displayProps: { key: 'subissue', fixed: 'left' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Status,
|
||||
presenter: tracker.component.StatusEditor,
|
||||
props: { kind: 'list', size: 'small', justify: 'center' },
|
||||
displayProps: { key: 'substatus' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Title,
|
||||
presenter: tracker.component.TitlePresenter,
|
||||
props: { shouldUseMargin: true, showParent: false }
|
||||
},
|
||||
{ key: '', label: tracker.string.SubIssues, presenter: tracker.component.SubIssuesSelector, props: {} },
|
||||
{ key: '', displayProps: { grow: true } },
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Milestone,
|
||||
presenter: tracker.component.MilestoneEditor,
|
||||
props: {
|
||||
kind: 'list',
|
||||
size: 'small',
|
||||
shouldShowPlaceholder: false
|
||||
},
|
||||
displayProps: {
|
||||
excludeByKey: 'milestone',
|
||||
compression: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.DueDate,
|
||||
presenter: tracker.component.DueDatePresenter,
|
||||
displayProps: { key: 'dueDate', compression: true },
|
||||
props: { kind: 'list', size: 'small' }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
label: tracker.string.Estimation,
|
||||
presenter: tracker.component.EstimationEditor,
|
||||
displayProps: { optional: true },
|
||||
props: { kind: 'list', size: 'small' }
|
||||
},
|
||||
{
|
||||
key: 'modifiedOn',
|
||||
presenter: tracker.component.ModificationDatePresenter,
|
||||
displayProps: { key: 'submodified', fixed: 'right' }
|
||||
},
|
||||
{
|
||||
key: 'assignee',
|
||||
presenter: tracker.component.AssigneeEditor,
|
||||
displayProps: { key: 'assignee', fixed: 'right' },
|
||||
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' }
|
||||
}
|
||||
]
|
||||
config: issueConfig('sub', true, true)
|
||||
},
|
||||
tracker.viewlet.SubIssues
|
||||
)
|
||||
|
||||
const milestoneIssueOptions: ViewOptionsModel = {
|
||||
groupBy: ['status', 'assignee', 'priority', 'component', 'createdBy', 'modifiedBy'],
|
||||
orderBy: [
|
||||
['rank', SortingOrder.Ascending],
|
||||
['status', SortingOrder.Ascending],
|
||||
['priority', SortingOrder.Ascending],
|
||||
['modifiedOn', SortingOrder.Descending],
|
||||
['createdOn', SortingOrder.Descending],
|
||||
['dueDate', SortingOrder.Ascending]
|
||||
],
|
||||
groupDepth: 1,
|
||||
other: [showColorsViewOption]
|
||||
}
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: tracker.class.Issue,
|
||||
descriptor: view.viewlet.List,
|
||||
viewOptions: milestoneIssueOptions,
|
||||
variant: 'milestone',
|
||||
configOptions: {
|
||||
strict: true,
|
||||
hiddenKeys: [
|
||||
'priority',
|
||||
'number',
|
||||
'status',
|
||||
'title',
|
||||
'dueDate',
|
||||
'milestone',
|
||||
'estimation',
|
||||
'createdBy',
|
||||
'modifiedBy'
|
||||
]
|
||||
},
|
||||
config: issueConfig('sub', true, false, true)
|
||||
},
|
||||
tracker.viewlet.MilestoneIssuesList
|
||||
)
|
||||
|
||||
const componentIssueOptions: ViewOptionsModel = {
|
||||
groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'],
|
||||
orderBy: [
|
||||
['rank', SortingOrder.Ascending],
|
||||
['status', SortingOrder.Ascending],
|
||||
['priority', SortingOrder.Ascending],
|
||||
['modifiedOn', SortingOrder.Descending],
|
||||
['createdOn', SortingOrder.Descending],
|
||||
['dueDate', SortingOrder.Ascending]
|
||||
],
|
||||
groupDepth: 1,
|
||||
other: [showColorsViewOption]
|
||||
}
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: tracker.class.Issue,
|
||||
descriptor: view.viewlet.List,
|
||||
viewOptions: componentIssueOptions,
|
||||
variant: 'component',
|
||||
configOptions: {
|
||||
strict: true,
|
||||
hiddenKeys: [
|
||||
'priority',
|
||||
'number',
|
||||
'status',
|
||||
'title',
|
||||
'dueDate',
|
||||
'component',
|
||||
'estimation',
|
||||
'createdBy',
|
||||
'modifiedBy'
|
||||
]
|
||||
},
|
||||
config: issueConfig('sub', true, true, false)
|
||||
},
|
||||
tracker.viewlet.ComponentIssuesList
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Component, ComponentExtensionId } from '@hcengineering/ui'
|
||||
import plugin from '../plugin'
|
||||
import { ComponentPointExtension } from '../types'
|
||||
import { getClient } from '../utils'
|
||||
|
||||
export let extension: ComponentExtensionId
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
let extensions: ComponentPointExtension[] = []
|
||||
|
||||
getClient()
|
||||
.findAll<ComponentPointExtension>(plugin.class.ComponentPointExtension, {
|
||||
extension
|
||||
})
|
||||
.then((res) => {
|
||||
extensions = res
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each extensions as extension}
|
||||
<Component is={extension.component} props={{ ...extension.props, ...props }} />
|
||||
{/each}
|
||||
@@ -42,6 +42,7 @@ export { default as NavLink } from './components/NavLink.svelte'
|
||||
export { default as IconForward } from './components/icons/Forward.svelte'
|
||||
export { default as Breadcrumbs } from './components/breadcrumbs/Breadcrumbs.svelte'
|
||||
export { default as BreadcrumbsElement } from './components/breadcrumbs/BreadcrumbsElement.svelte'
|
||||
export { default as ComponentExtensions } from './components/ComponentExtensions.svelte'
|
||||
export { default } from './plugin'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
import { Class, Ref } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import { ObjectSearchCategory } from './types'
|
||||
import { PresentationMiddlewareFactory } from './pipeline'
|
||||
import { ComponentPointExtension, ObjectSearchCategory } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -28,7 +28,8 @@ export const presentationId = 'presentation' as Plugin
|
||||
export default plugin(presentationId, {
|
||||
class: {
|
||||
ObjectSearchCategory: '' as Ref<Class<ObjectSearchCategory>>,
|
||||
PresentationMiddlewareFactory: '' as Ref<Class<PresentationMiddlewareFactory>>
|
||||
PresentationMiddlewareFactory: '' as Ref<Class<PresentationMiddlewareFactory>>,
|
||||
ComponentPointExtension: '' as Ref<Class<ComponentPointExtension>>
|
||||
},
|
||||
string: {
|
||||
Create: '' as IntlString,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Client, Doc, RelatedDocument } from '@hcengineering/core'
|
||||
import { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { AnyComponent, AnySvelteComponent, ComponentExtensionId } from '@hcengineering/ui'
|
||||
|
||||
export * from './components/breadcrumbs/types'
|
||||
|
||||
@@ -47,3 +47,22 @@ export interface ObjectSearchCategory extends Doc {
|
||||
// Query for documents with pattern
|
||||
query: Resource<ObjectSearchFactory>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* An component extension to various places of platform.
|
||||
*/
|
||||
export interface ComponentPointExtension extends Doc {
|
||||
// Extension point we should extend.
|
||||
extension: ComponentExtensionId
|
||||
|
||||
// Component to be instantiated with at least following properties:
|
||||
// size: 'tiny' | 'small' | 'medium' | 'large'
|
||||
component: AnyComponent
|
||||
|
||||
// Extra properties to be passed to the component
|
||||
props?: Record<string, any>
|
||||
|
||||
order?: number // Positioning of elements, into groups.
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ const defineAvatarColor = (
|
||||
dark: boolean = false
|
||||
): ColorDefinition => {
|
||||
const background = rgbToHex(hslToRgb(h / 360, s / 100, l / 100))
|
||||
const icon = rgbToHex(hslToRgb(h / 360, 0.5, 0.6))
|
||||
const title = rgbToHex(hslToRgb(h / 360, 0.9, 0.9))
|
||||
const icon: string | undefined = rgbToHex(hslToRgb(h / 360, 0.5, 0.6))
|
||||
const title: string | undefined = rgbToHex(hslToRgb(h / 360, 0.9, 0.9))
|
||||
return {
|
||||
name,
|
||||
color: background,
|
||||
|
||||
@@ -59,6 +59,13 @@ export type AnySvelteComponent = ComponentType
|
||||
export type Component<C extends AnySvelteComponent> = Resource<C>
|
||||
export type AnyComponent = Resource<AnySvelteComponent>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Component extension points.
|
||||
*/
|
||||
export type ComponentExtensionId = string & { __componentPointId: true }
|
||||
|
||||
/**
|
||||
* Allow to pass component with some predefined properties.
|
||||
* @public
|
||||
|
||||
@@ -64,13 +64,16 @@
|
||||
modifiedOn: Date.now(),
|
||||
modifiedBy: '' as Ref<Account>,
|
||||
date: 0,
|
||||
dueDate: undefined,
|
||||
access: 'reader',
|
||||
allDay: false,
|
||||
description,
|
||||
application,
|
||||
company,
|
||||
verdict: '',
|
||||
title,
|
||||
participants: [currentUser.person]
|
||||
participants: [currentUser.person],
|
||||
eventId: '',
|
||||
dueDate: 0
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -113,7 +116,10 @@
|
||||
participants: doc.participants,
|
||||
company,
|
||||
application,
|
||||
location
|
||||
location,
|
||||
access: 'reader',
|
||||
allDay: false,
|
||||
eventId: ''
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Event } from '@hcengineering/calendar'
|
||||
import { Calendar, Event } from '@hcengineering/calendar'
|
||||
import type { Channel, Organization, Person } from '@hcengineering/contact'
|
||||
import type {
|
||||
AttachedData,
|
||||
@@ -197,7 +197,7 @@ const recruit = plugin(recruitId, {
|
||||
},
|
||||
space: {
|
||||
VacancyTemplates: '' as Ref<KanbanTemplateSpace>,
|
||||
Reviews: '' as Ref<Space>
|
||||
Reviews: '' as Ref<Calendar>
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"SubIssues": "Sub-issues",
|
||||
"SubIssuesList": "Sub-issues ({subIssues})",
|
||||
"OpenSubIssues": "Open sub-issues",
|
||||
"AddSubIssues": "{subIssues, plural, =1 {Add sub-issue} other {+ Add sub-issues}}",
|
||||
"AddSubIssues": "Add sub-issue",
|
||||
"BlockedBy": "Blocked by",
|
||||
"RelatedTo": "Related to",
|
||||
"Comments": "Comments",
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"SubIssues": "Подзадачи",
|
||||
"SubIssuesList": "Подзадачи ({subIssues})",
|
||||
"OpenSubIssues": "Открыть подзадачи",
|
||||
"AddSubIssues": "{subIssues, plural, =1 {Добавить подзадачу} other {+ Добавить подзадачи}}",
|
||||
"AddSubIssues": "Добавить подзадачу",
|
||||
"BlockedBy": "Блокируется",
|
||||
"RelatedTo": "Связано",
|
||||
"Comments": "Комментарии",
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Component } from '@hcengineering/tracker'
|
||||
import { EditBox } from '@hcengineering/ui'
|
||||
import { EditBox, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import QueryIssuesList from '../issues/edit/QueryIssuesList.svelte'
|
||||
|
||||
export let object: Component
|
||||
|
||||
@@ -53,3 +54,20 @@
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-6">
|
||||
<QueryIssuesList
|
||||
focusIndex={50}
|
||||
{object}
|
||||
query={{ component: object._id }}
|
||||
shouldSaveDraft
|
||||
hasSubIssues={true}
|
||||
viewletId={tracker.viewlet.ComponentIssuesList}
|
||||
createParams={{ component: object._id }}
|
||||
on:docs
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<Label label={tracker.string.Issues} />
|
||||
</svelte:fragment>
|
||||
</QueryIssuesList>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { FilterBar, SpaceHeader, ViewletContentView, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
import { ComponentExtensions } from '@hcengineering/presentation'
|
||||
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
@@ -51,7 +52,7 @@
|
||||
bind:viewlet
|
||||
bind:search
|
||||
showLabelSelector={$$slots.label_selector}
|
||||
viewletQuery={{ attachTo: tracker.class.Issue, variant: { $ne: 'subissue' } }}
|
||||
viewletQuery={{ attachTo: tracker.class.Issue, variant: { $nin: ['subissue', 'component', 'milestone'] } }}
|
||||
{viewlets}
|
||||
{label}
|
||||
{space}
|
||||
@@ -61,6 +62,11 @@
|
||||
<slot name="label_selector" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="extra">
|
||||
<ComponentExtensions
|
||||
extension={tracker.extensions.IssueListHeader}
|
||||
props={{ size: 'medium', kind: 'ghost', space }}
|
||||
/>
|
||||
|
||||
<ViewletSettingButton bind:viewOptions bind:viewlet />
|
||||
{#if asideFloat && $$slots.aside}
|
||||
<div class="buttons-divider" />
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<!--
|
||||
// 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 { Class, Doc, DocumentQuery, Ref, toIdMap } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, Project } from '@hcengineering/tracker'
|
||||
import { Button, Chevron, ExpandCollapse, IconAdd, closeTooltip, showPopup } from '@hcengineering/ui'
|
||||
import view, { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { ViewletsSettingButton } from '@hcengineering/view-resources'
|
||||
import { afterUpdate } from 'svelte'
|
||||
import tracker from '../../../plugin'
|
||||
import CreateIssue from '../../CreateIssue.svelte'
|
||||
import SubIssueList from './SubIssueList.svelte'
|
||||
|
||||
export let projects: Map<Ref<Project>, Project> | undefined = undefined
|
||||
export let shouldSaveDraft: boolean = false
|
||||
export let object: Doc
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
|
||||
export let createParams: Record<string, any> = {}
|
||||
export let viewletId = tracker.viewlet.SubIssues
|
||||
export let createLabel = tracker.string.CreatedIssue
|
||||
export let hasSubIssues = false
|
||||
|
||||
let isCollapsed = false
|
||||
let listWidth: number
|
||||
|
||||
let viewlet: Viewlet | undefined
|
||||
let viewOptions: ViewOptions | undefined
|
||||
let _projects = projects
|
||||
export let focusIndex = -1
|
||||
|
||||
const projectsQuery = createQuery()
|
||||
|
||||
function openNewIssueDialog (): void {
|
||||
showPopup(tracker.component.CreateIssue, { space: object.space, ...createParams, shouldSaveDraft }, 'top')
|
||||
}
|
||||
|
||||
$: if (projects === undefined) {
|
||||
projectsQuery.query(tracker.class.Project, { archived: false }, async (result) => {
|
||||
_projects = toIdMap(result)
|
||||
})
|
||||
} else {
|
||||
projectsQuery.unsubscribe()
|
||||
}
|
||||
|
||||
let lastIssueId: Ref<Doc>
|
||||
afterUpdate(() => {
|
||||
if (lastIssueId !== object._id) {
|
||||
lastIssueId = object._id
|
||||
}
|
||||
})
|
||||
|
||||
const preferenceQuery = createQuery()
|
||||
const objectConfigurations = createQuery()
|
||||
let preference: ViewletPreference[] = []
|
||||
let loading = true
|
||||
|
||||
let configurationRaw: Viewlet[] = []
|
||||
let configurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
$: viewlet &&
|
||||
objectConfigurations.query(
|
||||
view.class.Viewlet,
|
||||
{
|
||||
attachTo: { $in: client.getHierarchy().getDescendants(viewlet.attachTo) },
|
||||
descriptor: viewlet.descriptor,
|
||||
variant: viewlet.variant ? viewlet.variant : { $exists: false }
|
||||
},
|
||||
(res) => {
|
||||
configurationRaw = res
|
||||
loading = false
|
||||
}
|
||||
)
|
||||
|
||||
$: viewlet &&
|
||||
preferenceQuery.query(
|
||||
view.class.ViewletPreference,
|
||||
{
|
||||
attachedTo: { $in: configurationRaw.map((it) => it._id) }
|
||||
},
|
||||
(res) => {
|
||||
preference = res
|
||||
loading = false
|
||||
}
|
||||
)
|
||||
|
||||
function updateConfiguration (configurationRaw: Viewlet[], preference: ViewletPreference[]): void {
|
||||
const newConfigurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
|
||||
|
||||
for (const v of configurationRaw) {
|
||||
newConfigurations[v.attachTo] = v.config
|
||||
}
|
||||
|
||||
// Add viewlet configurations.
|
||||
for (const pref of preference) {
|
||||
if (pref.config.length > 0) {
|
||||
const viewlet = configurationRaw.find((it) => it._id === pref.attachedTo)
|
||||
if (viewlet !== undefined) {
|
||||
newConfigurations[viewlet.attachTo] = pref.config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations = newConfigurations
|
||||
}
|
||||
|
||||
$: updateConfiguration(configurationRaw, preference)
|
||||
</script>
|
||||
|
||||
<div class="flex-between mb-1">
|
||||
{#if hasSubIssues}
|
||||
{#if $$slots.header}
|
||||
<slot name="header" />
|
||||
{:else}
|
||||
<Button
|
||||
width="min-content"
|
||||
kind="ghost"
|
||||
on:click={() => {
|
||||
isCollapsed = !isCollapsed
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<Chevron
|
||||
size={'small'}
|
||||
expanded={!isCollapsed}
|
||||
outline
|
||||
fill={'var(--caption-color)'}
|
||||
marginRight={'.375rem'}
|
||||
/>
|
||||
<slot name="chevron" />
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="flex-row-center gap-2">
|
||||
{#if hasSubIssues}
|
||||
<ViewletsSettingButton bind:viewOptions viewletQuery={{ _id: viewletId }} kind={'ghost'} bind:viewlet />
|
||||
{/if}
|
||||
{#if hasSubIssues}
|
||||
<slot name="buttons" />
|
||||
{/if}
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
icon={IconAdd}
|
||||
label={hasSubIssues ? undefined : createLabel}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'ghost'}
|
||||
showTooltip={{ label: createLabel, direction: 'bottom' }}
|
||||
on:click={() => {
|
||||
isCollapsed = false
|
||||
closeTooltip()
|
||||
openNewIssueDialog()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if hasSubIssues && viewOptions && viewlet}
|
||||
{#if !isCollapsed}
|
||||
<ExpandCollapse isExpanded={!isCollapsed}>
|
||||
<div class="list" class:collapsed={isCollapsed} bind:clientWidth={listWidth}>
|
||||
<SubIssueList
|
||||
createItemDialog={CreateIssue}
|
||||
createItemLabel={tracker.string.AddIssueTooltip}
|
||||
createItemDialogProps={{ space: object.space, ...createParams, shouldSaveDraft }}
|
||||
focusIndex={focusIndex === -1 ? -1 : focusIndex + 1}
|
||||
projects={_projects}
|
||||
{configurations}
|
||||
{preference}
|
||||
{viewlet}
|
||||
{viewOptions}
|
||||
{query}
|
||||
compactMode={listWidth <= 600}
|
||||
on:docs
|
||||
/>
|
||||
</div>
|
||||
</ExpandCollapse>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.list {
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
||||
&.collapsed {
|
||||
padding-top: 1px;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -21,6 +21,7 @@
|
||||
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { List, ListSelectionProvider, SelectDirection, selectionStore } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let query: DocumentQuery<Issue> | undefined = undefined
|
||||
export let viewlet: Viewlet
|
||||
@@ -66,6 +67,8 @@
|
||||
export let createItemDialog: AnySvelteComponent | AnyComponent | undefined = undefined
|
||||
export let createItemLabel: IntlString | undefined = undefined
|
||||
export let createItemDialogProps: Record<string, any> | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@@ -100,6 +103,7 @@
|
||||
on:content={(evt) => {
|
||||
docs = evt.detail
|
||||
listProvider.update(evt.detail)
|
||||
dispatch('docs', docs)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -13,213 +13,58 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, Ref, toIdMap } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Issue, Project, trackerId } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
Chevron,
|
||||
ExpandCollapse,
|
||||
IconAdd,
|
||||
IconScaleFull,
|
||||
Label,
|
||||
closeTooltip,
|
||||
getCurrentResolvedLocation,
|
||||
navigate,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import view, { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||
import { ViewletsSettingButton, createFilter, setFilters } from '@hcengineering/view-resources'
|
||||
import { afterUpdate } from 'svelte'
|
||||
import { Button, IconScaleFull, Label, closeTooltip, getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
|
||||
import { createFilter, setFilters } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import CreateIssue from '../../CreateIssue.svelte'
|
||||
import SubIssueList from './SubIssueList.svelte'
|
||||
import QueryIssuesList from './QueryIssuesList.svelte'
|
||||
|
||||
export let issue: Issue
|
||||
export let projects: Map<Ref<Project>, Project>
|
||||
export let projects: Map<Ref<Project>, Project> | undefined
|
||||
export let shouldSaveDraft: boolean = false
|
||||
|
||||
let isCollapsed = false
|
||||
let listWidth: number
|
||||
|
||||
$: hasSubIssues = issue.subIssues > 0
|
||||
|
||||
let viewlet: Viewlet | undefined
|
||||
let viewOptions: ViewOptions | undefined
|
||||
let _projects = projects
|
||||
|
||||
const projectsQuery = createQuery()
|
||||
|
||||
function openNewIssueDialog (): void {
|
||||
showPopup(tracker.component.CreateIssue, { space: issue.space, parentIssue: issue, shouldSaveDraft }, 'top')
|
||||
}
|
||||
|
||||
$: if (projects === undefined) {
|
||||
projectsQuery.query(tracker.class.Project, {}, async (result) => {
|
||||
_projects = toIdMap(result)
|
||||
})
|
||||
} else {
|
||||
projectsQuery.unsubscribe()
|
||||
}
|
||||
|
||||
// showPopup(tracker.component.CreateIssue, { space: issue.space, parentIssue: issue, shouldSaveDraft }, 'top')
|
||||
export let focusIndex = -1
|
||||
|
||||
let lastIssueId: Ref<Issue>
|
||||
afterUpdate(() => {
|
||||
if (lastIssueId !== issue._id) {
|
||||
lastIssueId = issue._id
|
||||
}
|
||||
})
|
||||
|
||||
const preferenceQuery = createQuery()
|
||||
const objectConfigurations = createQuery()
|
||||
let preference: ViewletPreference[] = []
|
||||
let loading = true
|
||||
|
||||
let configurationRaw: Viewlet[] = []
|
||||
let configurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
$: viewlet &&
|
||||
objectConfigurations.query(
|
||||
view.class.Viewlet,
|
||||
{
|
||||
attachTo: { $in: client.getHierarchy().getDescendants(viewlet.attachTo) },
|
||||
descriptor: viewlet.descriptor,
|
||||
variant: viewlet.variant ? viewlet.variant : { $exists: false }
|
||||
},
|
||||
(res) => {
|
||||
configurationRaw = res
|
||||
loading = false
|
||||
}
|
||||
)
|
||||
|
||||
$: viewlet &&
|
||||
preferenceQuery.query(
|
||||
view.class.ViewletPreference,
|
||||
{
|
||||
attachedTo: { $in: configurationRaw.map((it) => it._id) }
|
||||
},
|
||||
(res) => {
|
||||
preference = res
|
||||
loading = false
|
||||
}
|
||||
)
|
||||
|
||||
function updateConfiguration (configurationRaw: Viewlet[], preference: ViewletPreference[]): void {
|
||||
const newConfigurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
|
||||
|
||||
for (const v of configurationRaw) {
|
||||
newConfigurations[v.attachTo] = v.config
|
||||
}
|
||||
|
||||
// Add viewlet configurations.
|
||||
for (const pref of preference) {
|
||||
if (pref.config.length > 0) {
|
||||
const viewlet = configurationRaw.find((it) => it._id === pref.attachedTo)
|
||||
if (viewlet !== undefined) {
|
||||
newConfigurations[viewlet.attachTo] = pref.config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations = newConfigurations
|
||||
}
|
||||
|
||||
$: updateConfiguration(configurationRaw, preference)
|
||||
let size = issue.subIssues
|
||||
</script>
|
||||
|
||||
<div class="flex-between mb-1">
|
||||
{#if hasSubIssues}
|
||||
<QueryIssuesList
|
||||
object={issue}
|
||||
query={{ attachedTo: issue._id }}
|
||||
createParams={{ space: issue.space, parentIssue: issue }}
|
||||
createLabel={tracker.string.AddSubIssues}
|
||||
hasSubIssues={issue.subIssues > 0}
|
||||
{focusIndex}
|
||||
{projects}
|
||||
{shouldSaveDraft}
|
||||
on:docs={(evt) => {
|
||||
size = evt.detail.length
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="chevron">
|
||||
<Label label={tracker.string.SubIssuesList} params={{ subIssues: size }} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
width="min-content"
|
||||
kind="ghost"
|
||||
on:click={() => {
|
||||
isCollapsed = !isCollapsed
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<Chevron size={'small'} expanded={!isCollapsed} outline fill={'var(--caption-color)'} marginRight={'.375rem'} />
|
||||
<Label label={tracker.string.SubIssuesList} params={{ subIssues: issue.subIssues }} />
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex-row-center gap-2">
|
||||
{#if hasSubIssues}
|
||||
<ViewletsSettingButton
|
||||
bind:viewOptions
|
||||
viewletQuery={{ _id: tracker.viewlet.SubIssues }}
|
||||
kind={'ghost'}
|
||||
bind:viewlet
|
||||
/>
|
||||
{/if}
|
||||
{#if hasSubIssues}
|
||||
<Button
|
||||
icon={IconScaleFull}
|
||||
kind={'ghost'}
|
||||
showTooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }}
|
||||
on:click={() => {
|
||||
const filter = createFilter(tracker.class.Issue, 'attachedTo', [issue._id])
|
||||
if (filter !== undefined) {
|
||||
closeTooltip()
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.fragment = undefined
|
||||
loc.query = undefined
|
||||
loc.path[2] = trackerId
|
||||
loc.path[3] = issue.space
|
||||
loc.path[4] = 'issues'
|
||||
navigate(loc)
|
||||
setFilters([filter])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
icon={hasSubIssues ? IconAdd : undefined}
|
||||
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
icon={IconScaleFull}
|
||||
kind={'ghost'}
|
||||
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
|
||||
showTooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }}
|
||||
on:click={() => {
|
||||
isCollapsed = false
|
||||
closeTooltip()
|
||||
openNewIssueDialog()
|
||||
const filter = createFilter(tracker.class.Issue, 'attachedTo', [issue._id])
|
||||
if (filter !== undefined) {
|
||||
closeTooltip()
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.fragment = undefined
|
||||
loc.query = undefined
|
||||
loc.path[2] = trackerId
|
||||
loc.path[3] = issue.space
|
||||
loc.path[4] = 'issues'
|
||||
navigate(loc)
|
||||
setFilters([filter])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if hasSubIssues && viewOptions && viewlet}
|
||||
{#if !isCollapsed}
|
||||
<ExpandCollapse isExpanded={!isCollapsed}>
|
||||
<div class="list" class:collapsed={isCollapsed} bind:clientWidth={listWidth}>
|
||||
<SubIssueList
|
||||
createItemDialog={CreateIssue}
|
||||
createItemLabel={tracker.string.AddIssueTooltip}
|
||||
createItemDialogProps={{ space: issue.space, parentIssue: issue, shouldSaveDraft }}
|
||||
focusIndex={focusIndex === -1 ? -1 : focusIndex + 1}
|
||||
projects={_projects}
|
||||
{configurations}
|
||||
{preference}
|
||||
{viewlet}
|
||||
{viewOptions}
|
||||
query={{ attachedTo: issue._id }}
|
||||
compactMode={listWidth <= 600}
|
||||
/>
|
||||
</div>
|
||||
</ExpandCollapse>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.list {
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
||||
&.collapsed {
|
||||
padding-top: 1px;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</svelte:fragment>
|
||||
</QueryIssuesList>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
const projectsQuery = createQuery()
|
||||
|
||||
$: projectsQuery.query(tracker.class.Project, {}, async (result) => {
|
||||
$: projectsQuery.query(tracker.class.Project, { archived: false }, async (result) => {
|
||||
projects = new Map(result.map((it) => [it._id, it]))
|
||||
})
|
||||
</script>
|
||||
|
||||
+14
-71
@@ -1,86 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Doc, DocumentQuery } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { configurationStore, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, trackerId } from '@hcengineering/tracker'
|
||||
import { Button, Component, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
||||
import { ViewOptions, Viewlet } from '@hcengineering/view'
|
||||
import { ViewletsSettingButton, getAdditionalHeader } from '@hcengineering/view-resources'
|
||||
import viewplg from '@hcengineering/view-resources/src/plugin'
|
||||
import { fade } from 'svelte/transition'
|
||||
import tracker from '../../../plugin'
|
||||
import RelatedIssues from './RelatedIssues.svelte'
|
||||
import { configurationStore } from '@hcengineering/presentation'
|
||||
import tracker, { Issue, trackerId } from '@hcengineering/tracker'
|
||||
import { Icon, Label } from '@hcengineering/ui'
|
||||
import QueryIssuesList from '../edit/QueryIssuesList.svelte'
|
||||
|
||||
export let object: Doc
|
||||
export let label: IntlString
|
||||
|
||||
const client = getClient()
|
||||
let viewlet: Viewlet | undefined
|
||||
let listWidth: number
|
||||
|
||||
let viewOptions: ViewOptions | undefined
|
||||
const createIssue = () => showPopup(tracker.component.CreateIssue, { relatedTo: object, space: object.space }, 'top')
|
||||
|
||||
let query: DocumentQuery<Issue>
|
||||
$: query = { 'relations._id': object._id, 'relations._class': object._class }
|
||||
const subIssuesQuery = createQuery()
|
||||
let subIssues: Issue[] = []
|
||||
$: subIssuesQuery.query(tracker.class.Issue, query, async (result) => (subIssues = result))
|
||||
|
||||
$: headerRemoval = viewOptions?.groupBy?.length === 0 || viewOptions?.groupBy?.[0] === '#no_category'
|
||||
$: extraHeaders = headerRemoval ? getAdditionalHeader(client, tracker.class.Issue) : undefined
|
||||
</script>
|
||||
|
||||
{#if $configurationStore.has(trackerId)}
|
||||
<div class="antiSection" bind:clientWidth={listWidth}>
|
||||
<div class="antiSection-header mb-3">
|
||||
<div class="antiSection-header__icon">
|
||||
<Icon icon={tracker.icon.Issue} size={'small'} />
|
||||
</div>
|
||||
<span class="antiSection-header__title short overflow-label">
|
||||
<Label {label} />
|
||||
</span>
|
||||
{#if headerRemoval}
|
||||
<div in:fade|local={{ duration: 150 }} class="antiSection-header__header flex-between">
|
||||
<span class="content-dark-color"><Label label={viewplg.string.NoGrouping} /></span>
|
||||
<div class="buttons-group font-normal text-normal">
|
||||
{#if extraHeaders}
|
||||
{#each extraHeaders as extra}
|
||||
<Component is={extra} props={{ docs: subIssues }} />
|
||||
{/each}
|
||||
{/if}
|
||||
<span class="antiSection-header__counter">{subIssues.length}</span>
|
||||
</div>
|
||||
<QueryIssuesList {object} {query} createParams={{ relatedTo: object }} hasSubIssues>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="flex-row-center mb-3">
|
||||
<div class="antiSection-header__icon">
|
||||
<Icon icon={tracker.icon.Issue} size={'small'} />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="flex-grow" />
|
||||
{/if}
|
||||
<div class="flex-row-center gap-2">
|
||||
<ViewletsSettingButton
|
||||
bind:viewOptions
|
||||
viewletQuery={{ _id: tracker.viewlet.SubIssues }}
|
||||
kind={'ghost'}
|
||||
bind:viewlet
|
||||
/>
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
icon={IconAdd}
|
||||
label={undefined}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'ghost'}
|
||||
on:click={createIssue}
|
||||
/>
|
||||
<span class="antiSection-header__title short">
|
||||
<Label {label} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if viewlet && viewOptions}
|
||||
<RelatedIssues
|
||||
{object}
|
||||
{viewOptions}
|
||||
{viewlet}
|
||||
on:add-issue={createIssue}
|
||||
disableHeader={headerRemoval}
|
||||
compactMode={listWidth <= 600}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</QueryIssuesList>
|
||||
{/if}
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Milestone } from '@hcengineering/tracker'
|
||||
import { EditBox } from '@hcengineering/ui'
|
||||
import { EditBox, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
|
||||
import QueryIssuesList from '../issues/edit/QueryIssuesList.svelte'
|
||||
|
||||
export let object: Milestone
|
||||
|
||||
@@ -66,3 +67,19 @@
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-6">
|
||||
<QueryIssuesList
|
||||
focusIndex={50}
|
||||
{object}
|
||||
query={{ milestone: object._id }}
|
||||
shouldSaveDraft
|
||||
hasSubIssues={true}
|
||||
viewletId={tracker.viewlet.MilestoneIssuesList}
|
||||
createParams={{ milestone: object._id }}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<Label label={tracker.string.Issues} />
|
||||
</svelte:fragment>
|
||||
</QueryIssuesList>
|
||||
</div>
|
||||
|
||||
@@ -75,10 +75,9 @@
|
||||
width="min-content"
|
||||
icon={hasSubIssues ? IconAdd : undefined}
|
||||
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 } }}
|
||||
showTooltip={{ label: tracker.string.AddSubIssues }}
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
isCreating = true
|
||||
|
||||
@@ -32,7 +32,9 @@ export default mergeIds(trackerId, tracker, {
|
||||
viewlet: {
|
||||
SubIssues: '' as Ref<Viewlet>,
|
||||
List: '' as Ref<ViewletDescriptor>,
|
||||
Kanban: '' as Ref<ViewletDescriptor>
|
||||
Kanban: '' as Ref<ViewletDescriptor>,
|
||||
MilestoneIssuesList: '' as Ref<Viewlet>,
|
||||
ComponentIssuesList: '' as Ref<Viewlet>
|
||||
},
|
||||
string: {
|
||||
More: '' as IntlString,
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
import { Asset, IntlString, Plugin, Resource, plugin } from '@hcengineering/platform'
|
||||
import { TagCategory, TagElement, TagReference } from '@hcengineering/tags'
|
||||
import { SpaceWithStates, Task } from '@hcengineering/task'
|
||||
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
|
||||
import { AnyComponent, ComponentExtensionId, Location, ResolvedLocation } from '@hcengineering/ui'
|
||||
import { Action, ActionCategory, IconProps } from '@hcengineering/view'
|
||||
|
||||
/**
|
||||
@@ -485,5 +485,8 @@ export default plugin(trackerId, {
|
||||
},
|
||||
mixin: {
|
||||
ProjectIssueTargetOptions: '' as Ref<Mixin<ProjectIssueTargetOptions>>
|
||||
},
|
||||
extensions: {
|
||||
IssueListHeader: '' as ComponentExtensionId
|
||||
}
|
||||
})
|
||||
|
||||
@@ -625,6 +625,12 @@ export async function restore (
|
||||
model: 'upgrade'
|
||||
})) as unknown as CoreClient & BackupClient
|
||||
|
||||
// We need to find empty domains and clean them.
|
||||
const allDomains = connection.getHierarchy().domains()
|
||||
for (const d of allDomains) {
|
||||
domains.add(d)
|
||||
}
|
||||
|
||||
async function processDomain (c: Domain): Promise<void> {
|
||||
const changeset = await loadDigest(storage, snapshots, c, date)
|
||||
// We need to load full changeset from server
|
||||
|
||||
@@ -8,7 +8,7 @@ test.use({
|
||||
test.describe('recruit review tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Create user and workspace
|
||||
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
|
||||
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/recruit`))?.finished()
|
||||
})
|
||||
test('create-review', async ({ page, context }) => {
|
||||
await page.click('[id="app-recruit\\:string\\:RecruitApplication"]')
|
||||
|
||||
@@ -8,7 +8,7 @@ test.use({
|
||||
test.describe('recruit tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Create user and workspace
|
||||
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
|
||||
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/recruit`))?.finished()
|
||||
})
|
||||
test('create-candidate', async ({ page, context }) => {
|
||||
await page.locator('[id="app-recruit\\:string\\:RecruitApplication"]').click()
|
||||
|
||||
@@ -53,7 +53,7 @@ async function createComponents (page: Page): Promise<string[]> {
|
||||
const components = []
|
||||
|
||||
for (let index = 0; index < 5; index++) {
|
||||
const prjId = `component-${generateId()}-${index}`
|
||||
const prjId = `c-${generateId(5)}-${index}`
|
||||
components.push(prjId)
|
||||
|
||||
await createComponent(page, prjId)
|
||||
@@ -66,7 +66,7 @@ async function createMilestones (page: Page): Promise<string[]> {
|
||||
const milestones = []
|
||||
|
||||
for (let index = 0; index < 5; index++) {
|
||||
const milestoneId = `milestone-${generateId()}-${index}`
|
||||
const milestoneId = `m-${generateId(5)}-${index}`
|
||||
milestones.push(milestoneId)
|
||||
|
||||
await createMilestone(page, milestoneId)
|
||||
|
||||
Reference in New Issue
Block a user