UBER-799: Allow extensions to tracker for github (#3727)

This commit is contained in:
Andrey Sobolev
2023-09-21 19:20:17 +07:00
committed by GitHub
parent b18c7ab158
commit c7bb5b9cab
28 changed files with 613 additions and 467 deletions
+1 -1
View File
@@ -1 +1 @@
{ "major": 0, "minor": 6, "patch": 120 }
{ "major": 0, "minor": 6, "patch": 130 }
+16 -3
View File
@@ -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
View File
@@ -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}
+1
View File
@@ -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'
+3 -2
View File
@@ -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,
+20 -1
View File
@@ -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.
}
+2 -2
View File
@@ -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,
+7
View File
@@ -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: ''
})
}
+2 -2
View File
@@ -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>
}
})
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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>
@@ -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
+3 -1
View File
@@ -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,
+4 -1
View File
@@ -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
}
})
+6
View File
@@ -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
+1 -1
View File
@@ -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"]')
+1 -1
View File
@@ -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()
+2 -2
View File
@@ -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)