List extract (#2505)

This commit is contained in:
Denis Bykhov 2023-01-14 16:54:54 +06:00 committed by GitHub
parent 1c01925ed4
commit 6a5f1ed479
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
124 changed files with 2382 additions and 1445 deletions

View File

@ -77,7 +77,7 @@ export class TSavedAttachments extends TPreference implements SavedAttachments {
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TAttachment, TPhoto, TSavedAttachments) builder.createModel(TAttachment, TPhoto, TSavedAttachments)
builder.mixin(attachment.class.Attachment, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(attachment.class.Attachment, core.class.Class, view.mixin.ObjectPresenter, {
presenter: attachment.component.AttachmentPresenter presenter: attachment.component.AttachmentPresenter
}) })

View File

@ -212,15 +212,15 @@ export function createModel (builder: Builder): void {
editor: board.component.EditCard editor: board.component.EditCard
}) })
builder.mixin(board.class.Card, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(board.class.Card, core.class.Class, view.mixin.ObjectPresenter, {
presenter: board.component.CardPresenter presenter: board.component.CardPresenter
}) })
builder.mixin(board.class.Board, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(board.class.Board, core.class.Class, view.mixin.ObjectPresenter, {
presenter: board.component.BoardPresenter presenter: board.component.BoardPresenter
}) })
builder.mixin(board.class.CardCover, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(board.class.CardCover, core.class.Class, view.mixin.ObjectPresenter, {
presenter: board.component.CardCoverPresenter presenter: board.component.CardCoverPresenter
}) })

View File

@ -187,7 +187,7 @@ export function createModel (builder: Builder): void {
calendar.action.SaveEventReminder calendar.action.SaveEventReminder
) )
builder.mixin(calendar.mixin.Reminder, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(calendar.mixin.Reminder, core.class.Class, view.mixin.ObjectPresenter, {
presenter: calendar.component.ReminderPresenter presenter: calendar.component.ReminderPresenter
}) })
@ -195,7 +195,7 @@ export function createModel (builder: Builder): void {
editor: calendar.component.EditEvent editor: calendar.component.EditEvent
}) })
builder.mixin(calendar.class.Event, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(calendar.class.Event, core.class.Class, view.mixin.ObjectPresenter, {
presenter: calendar.component.EventPresenter presenter: calendar.component.EventPresenter
}) })
} }

View File

@ -190,11 +190,11 @@ export function createModel (builder: Builder, options = { addApplication: true
getName: chunter.function.GetDmName getName: chunter.function.GetDmName
}) })
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectPresenter, {
presenter: chunter.component.DmPresenter presenter: chunter.component.DmPresenter
}) })
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectPresenter, {
presenter: chunter.component.ChannelPresenter presenter: chunter.component.ChannelPresenter
}) })
@ -404,7 +404,7 @@ export function createModel (builder: Builder, options = { addApplication: true
) )
} }
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.ObjectPresenter, {
presenter: chunter.component.CommentPresenter presenter: chunter.component.CommentPresenter
}) })

View File

@ -48,7 +48,7 @@ import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter' import chunter from '@hcengineering/model-chunter'
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@hcengineering/model-core' import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@hcengineering/model-core'
import presentation from '@hcengineering/model-presentation' import presentation from '@hcengineering/model-presentation'
import view, { actionTemplates, createAction, ViewAction } from '@hcengineering/model-view' import view, { actionTemplates, createAction, ViewAction, Viewlet } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import type { Asset, IntlString, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
@ -204,7 +204,7 @@ export function createModel (builder: Builder): void {
contact.app.Contacts contact.app.Contacts
) )
builder.createDoc( builder.createDoc<Viewlet>(
view.class.Viewlet, view.class.Viewlet,
core.space.Model, core.space.Model,
{ {
@ -229,7 +229,7 @@ export function createModel (builder: Builder): void {
pinned: true pinned: true
}) })
builder.createDoc( builder.createDoc<Viewlet>(
view.class.Viewlet, view.class.Viewlet,
core.space.Model, core.space.Model,
{ {
@ -237,7 +237,7 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.Table, descriptor: view.viewlet.Table,
config: [ config: [
'', '',
'$lookup._class', '_class',
'city', 'city',
'attachments', 'attachments',
'modifiedOn', 'modifiedOn',
@ -280,7 +280,7 @@ export function createModel (builder: Builder): void {
inlineEditor: contact.component.EmployeeArrayEditor inlineEditor: contact.component.EmployeeArrayEditor
}) })
builder.mixin(contact.class.Member, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(contact.class.Member, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.MemberPresenter presenter: contact.component.MemberPresenter
}) })
@ -292,7 +292,7 @@ export function createModel (builder: Builder): void {
inlineEditor: contact.component.EmployeeEditor inlineEditor: contact.component.EmployeeEditor
}) })
builder.mixin(contact.class.Channel, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(contact.class.Channel, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.ChannelsPresenter presenter: contact.component.ChannelsPresenter
}) })
@ -401,7 +401,7 @@ export function createModel (builder: Builder): void {
contact.avatarProvider.Gravatar contact.avatarProvider.Gravatar
) )
builder.mixin(contact.class.Person, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.PersonPresenter presenter: contact.component.PersonPresenter
}) })
@ -413,22 +413,38 @@ export function createModel (builder: Builder): void {
inlineEditor: contact.component.AccountArrayEditor inlineEditor: contact.component.AccountArrayEditor
}) })
builder.mixin(core.class.Account, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(core.class.Account, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.EmployeeAccountPresenter presenter: contact.component.EmployeeAccountPresenter
}) })
builder.mixin(contact.class.Organization, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(contact.class.Organization, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.OrganizationPresenter presenter: contact.component.OrganizationPresenter
}) })
builder.mixin(contact.class.Contact, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(contact.class.Contact, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.ContactPresenter presenter: contact.component.ContactPresenter
}) })
builder.mixin(contact.class.Employee, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(contact.class.Employee, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.EmployeePresenter presenter: contact.component.EmployeePresenter
}) })
builder.mixin(contact.class.Employee, core.class.Class, view.mixin.SortFuncs, {
func: contact.function.EmployeeSort
})
builder.mixin(contact.class.Person, core.class.Class, view.mixin.AttributePresenter, {
presenter: contact.component.PersonRefPresenter
})
builder.mixin(contact.class.Contact, core.class.Class, view.mixin.AttributePresenter, {
presenter: contact.component.ContactRefPresenter
})
builder.mixin(contact.class.Employee, core.class.Class, view.mixin.AttributePresenter, {
presenter: contact.component.EmployeeRefPresenter
})
builder.mixin(contact.class.Employee, core.class.Class, view.mixin.IgnoreActions, { builder.mixin(contact.class.Employee, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Delete] actions: [view.action.Delete]
}) })

View File

@ -25,6 +25,7 @@ import { Action, ActionCategory, ViewAction } from '@hcengineering/view'
export default mergeIds(contactId, contact, { export default mergeIds(contactId, contact, {
component: { component: {
PersonPresenter: '' as AnyComponent, PersonPresenter: '' as AnyComponent,
ContactRefPresenter: '' as AnyComponent,
ContactPresenter: '' as AnyComponent, ContactPresenter: '' as AnyComponent,
EditPerson: '' as AnyComponent, EditPerson: '' as AnyComponent,
EditOrganization: '' as AnyComponent, EditOrganization: '' as AnyComponent,
@ -35,6 +36,8 @@ export default mergeIds(contactId, contact, {
EmployeeAccountPresenter: '' as AnyComponent, EmployeeAccountPresenter: '' as AnyComponent,
OrganizationEditor: '' as AnyComponent, OrganizationEditor: '' as AnyComponent,
EmployeePresenter: '' as AnyComponent, EmployeePresenter: '' as AnyComponent,
EmployeeRefPresenter: '' as AnyComponent,
PersonRefPresenter: '' as AnyComponent,
PersonEditor: '' as AnyComponent, PersonEditor: '' as AnyComponent,
Members: '' as AnyComponent, Members: '' as AnyComponent,
MemberPresenter: '' as AnyComponent, MemberPresenter: '' as AnyComponent,

View File

@ -224,10 +224,10 @@ export function createModel (builder: Builder): void {
editor: document.component.EditDoc editor: document.component.EditDoc
}) })
builder.mixin(document.class.Document, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(document.class.Document, core.class.Class, view.mixin.ObjectPresenter, {
presenter: document.component.DocumentPresenter presenter: document.component.DocumentPresenter
}) })
builder.mixin(document.class.DocumentVersion, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(document.class.DocumentVersion, core.class.Class, view.mixin.ObjectPresenter, {
presenter: document.component.DocumentVersionPresenter presenter: document.component.DocumentVersionPresenter
}) })

View File

@ -387,7 +387,7 @@ export function createModel (builder: Builder): void {
} }
}) })
builder.mixin(hr.class.Request, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(hr.class.Request, core.class.Class, view.mixin.ObjectPresenter, {
presenter: hr.component.RequestPresenter presenter: hr.component.RequestPresenter
}) })
} }

View File

@ -22,8 +22,7 @@ import { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import type {} from '@hcengineering/view' import view, { Viewlet } from '@hcengineering/view'
import view from '@hcengineering/view'
import inventory from './plugin' import inventory from './plugin'
export const DOMAIN_INVENTORY = 'inventory' as Domain export const DOMAIN_INVENTORY = 'inventory' as Domain
@ -75,15 +74,19 @@ export class TVariant extends TAttachedDoc implements Variant {
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TCategory, TProduct, TVariant) builder.createModel(TCategory, TProduct, TVariant)
builder.mixin(inventory.class.Category, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(inventory.class.Category, core.class.Class, view.mixin.ObjectPresenter, {
presenter: inventory.component.CategoryPresenter presenter: inventory.component.CategoryPresenter
}) })
builder.mixin(inventory.class.Product, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(inventory.class.Category, core.class.Class, view.mixin.AttributePresenter, {
presenter: inventory.component.CategoryRefPresenter
})
builder.mixin(inventory.class.Product, core.class.Class, view.mixin.ObjectPresenter, {
presenter: inventory.component.ProductPresenter presenter: inventory.component.ProductPresenter
}) })
builder.mixin(inventory.class.Variant, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(inventory.class.Variant, core.class.Class, view.mixin.ObjectPresenter, {
presenter: inventory.component.VariantPresenter presenter: inventory.component.VariantPresenter
}) })
@ -99,13 +102,13 @@ export function createModel (builder: Builder): void {
value: true value: true
}) })
builder.createDoc( builder.createDoc<Viewlet>(
view.class.Viewlet, view.class.Viewlet,
core.space.Model, core.space.Model,
{ {
attachTo: inventory.class.Product, attachTo: inventory.class.Product,
descriptor: view.viewlet.Table, descriptor: view.viewlet.Table,
config: ['', '$lookup.attachedTo', 'modifiedOn'] config: ['', 'attachedTo', 'modifiedOn']
}, },
inventory.viewlet.TableProduct inventory.viewlet.TableProduct
) )

View File

@ -36,6 +36,7 @@ export default mergeIds(inventoryId, inventory, {
CreateProduct: '' as AnyComponent, CreateProduct: '' as AnyComponent,
EditProduct: '' as AnyComponent, EditProduct: '' as AnyComponent,
CategoryPresenter: '' as AnyComponent, CategoryPresenter: '' as AnyComponent,
CategoryRefPresenter: '' as AnyComponent,
Variants: '' as AnyComponent, Variants: '' as AnyComponent,
ProductPresenter: '' as AnyComponent, ProductPresenter: '' as AnyComponent,
VariantPresenter: '' as AnyComponent VariantPresenter: '' as AnyComponent

View File

@ -189,7 +189,7 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.Table, descriptor: view.viewlet.Table,
config: [ config: [
'', '',
'$lookup._class', '_class',
'leads', 'leads',
'modifiedOn', 'modifiedOn',
{ {
@ -212,9 +212,9 @@ export function createModel (builder: Builder): void {
config: [ config: [
'', '',
'title', 'title',
'$lookup.attachedTo', 'attachedTo',
'$lookup.state', 'state',
'$lookup.doneState', 'doneState',
'attachments', 'attachments',
'comments', 'comments',
'modifiedOn', 'modifiedOn',
@ -227,6 +227,36 @@ export function createModel (builder: Builder): void {
lead.viewlet.TableLead lead.viewlet.TableLead
) )
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: lead.class.Lead,
descriptor: view.viewlet.List,
config: [
{ key: '', props: { fixed: 'left' } },
{ key: 'title', props: { fixed: 'left' } },
{ key: 'state', props: { fixed: 'left' } },
{ key: 'doneState', props: { fixed: 'left' } },
{ key: '', presenter: view.component.GrowPresenter },
'attachments',
'comments',
'assignee'
],
viewOptions: {
groupBy: ['assignee', 'state', 'attachedTo'],
orderBy: [
['assignee', -1],
['state', 1],
['attachedTo', 1],
['modifiedOn', -1]
],
other: []
}
},
lead.viewlet.ListLead
)
builder.createDoc(view.class.Viewlet, core.space.Model, { builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: lead.class.Lead, attachTo: lead.class.Lead,
descriptor: task.viewlet.Kanban, descriptor: task.viewlet.Kanban,
@ -254,7 +284,7 @@ export function createModel (builder: Builder): void {
editor: lead.component.EditLead editor: lead.component.EditLead
}) })
builder.mixin(lead.class.Lead, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(lead.class.Lead, core.class.Class, view.mixin.ObjectPresenter, {
presenter: lead.component.LeadPresenter presenter: lead.component.LeadPresenter
}) })

View File

@ -30,7 +30,6 @@ export default mergeIds(leadId, lead, {
LeadApplication: '' as IntlString, LeadApplication: '' as IntlString,
Lead: '' as IntlString, Lead: '' as IntlString,
Title: '' as IntlString, Title: '' as IntlString,
Assignee: '' as IntlString,
ManageFunnelStatuses: '' as IntlString, ManageFunnelStatuses: '' as IntlString,
FunnelBrowser: '' as IntlString, FunnelBrowser: '' as IntlString,
GotoLeadApplication: '' as IntlString GotoLeadApplication: '' as IntlString
@ -52,7 +51,8 @@ export default mergeIds(leadId, lead, {
}, },
viewlet: { viewlet: {
TableCustomer: '' as Ref<Viewlet>, TableCustomer: '' as Ref<Viewlet>,
TableLead: '' as Ref<Viewlet> TableLead: '' as Ref<Viewlet>,
ListLead: '' as Ref<Viewlet>
}, },
category: { category: {
Lead: '' as Ref<ActionCategory> Lead: '' as Ref<ActionCategory>

View File

@ -351,10 +351,10 @@ export function createModel (builder: Builder): void {
descriptor: task.viewlet.StatusTable, descriptor: task.viewlet.StatusTable,
config: [ config: [
'', '',
'$lookup.attachedTo', 'attachedTo',
'$lookup.assignee', 'assignee',
'$lookup.state', 'state',
'$lookup.doneState', 'doneState',
'attachments', 'attachments',
'comments', 'comments',
'modifiedOn', 'modifiedOn',
@ -415,7 +415,7 @@ export function createModel (builder: Builder): void {
editor: recruit.component.EditVacancy editor: recruit.component.EditVacancy
}) })
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectPresenter, {
presenter: recruit.component.ApplicationPresenter presenter: recruit.component.ApplicationPresenter
}) })
@ -423,7 +423,7 @@ export function createModel (builder: Builder): void {
presenter: recruit.component.ApplicationsPresenter presenter: recruit.component.ApplicationsPresenter
}) })
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectPresenter, {
presenter: recruit.component.VacancyPresenter presenter: recruit.component.VacancyPresenter
}) })

View File

@ -60,11 +60,11 @@ export function createReviewModel (builder: Builder): void {
editor: recruit.component.EditReview editor: recruit.component.EditReview
}) })
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(recruit.class.Review, core.class.Class, view.mixin.ObjectPresenter, {
presenter: recruit.component.ReviewPresenter presenter: recruit.component.ReviewPresenter
}) })
builder.mixin(recruit.class.Opinion, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(recruit.class.Opinion, core.class.Class, view.mixin.ObjectPresenter, {
presenter: recruit.component.OpinionPresenter presenter: recruit.component.OpinionPresenter
}) })

View File

@ -56,7 +56,7 @@ export function createModel (builder: Builder): void {
editor: request.component.EditRequest editor: request.component.EditRequest
}) })
builder.mixin(request.class.Request, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(request.class.Request, core.class.Class, view.mixin.ObjectPresenter, {
presenter: request.component.RequestPresenter presenter: request.component.RequestPresenter
}) })

View File

@ -106,10 +106,10 @@ export function createModel (builder: Builder): void {
inlineEditor: tags.component.TagsAttributeEditor inlineEditor: tags.component.TagsAttributeEditor
}) })
builder.mixin(tags.class.TagReference, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tags.class.TagReference, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tags.component.TagReferencePresenter presenter: tags.component.TagReferencePresenter
}) })
builder.mixin(tags.class.TagElement, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tags.class.TagElement, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tags.component.TagElementPresenter presenter: tags.component.TagElementPresenter
}) })

View File

@ -366,29 +366,20 @@ export function createModel (builder: Builder): void {
{ {
attachTo: task.class.Issue, attachTo: task.class.Issue,
descriptor: task.viewlet.StatusTable, descriptor: task.viewlet.StatusTable,
config: [ config: ['', 'name', 'assignee', 'state', 'doneState', 'attachments', 'comments', 'modifiedOn']
'',
'name',
'$lookup.assignee',
'$lookup.state',
'$lookup.doneState',
'attachments',
'comments',
'modifiedOn'
]
}, },
task.viewlet.TableIssue task.viewlet.TableIssue
) )
builder.mixin(task.class.Task, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(task.class.Task, core.class.Class, view.mixin.ObjectPresenter, {
presenter: view.component.ObjectPresenter presenter: view.component.ObjectPresenter
}) })
builder.mixin(task.class.Issue, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(task.class.Issue, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.TaskPresenter presenter: task.component.TaskPresenter
}) })
builder.mixin(task.class.KanbanTemplate, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(task.class.KanbanTemplate, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.KanbanTemplatePresenter presenter: task.component.KanbanTemplatePresenter
}) })
@ -462,10 +453,14 @@ export function createModel (builder: Builder): void {
inlineEditor: task.component.StateEditor inlineEditor: task.component.StateEditor
}) })
builder.mixin(task.class.State, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(task.class.State, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.StatePresenter presenter: task.component.StatePresenter
}) })
builder.mixin(task.class.State, core.class.Class, view.mixin.AttributePresenter, {
presenter: task.component.StateRefPresenter
})
builder.mixin(task.class.State, core.class.Class, view.mixin.IgnoreActions, { builder.mixin(task.class.State, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Delete] actions: [view.action.Delete]
}) })
@ -474,10 +469,14 @@ export function createModel (builder: Builder): void {
inlineEditor: task.component.DoneStateEditor inlineEditor: task.component.DoneStateEditor
}) })
builder.mixin(task.class.DoneState, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(task.class.DoneState, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.DoneStatePresenter presenter: task.component.DoneStatePresenter
}) })
builder.mixin(task.class.DoneState, core.class.Class, view.mixin.AttributePresenter, {
presenter: task.component.DoneStateRefPresenter
})
builder.createDoc( builder.createDoc(
view.class.ViewletDescriptor, view.class.ViewletDescriptor,
core.space.Model, core.space.Model,
@ -504,7 +503,7 @@ export function createModel (builder: Builder): void {
editor: task.component.Todos editor: task.component.Todos
}) })
builder.mixin(task.class.TodoItem, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(task.class.TodoItem, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.TodoItemPresenter presenter: task.component.TodoItemPresenter
}) })

View File

@ -58,7 +58,9 @@ export default mergeIds(taskId, task, {
TodoItemPresenter: '' as AnyComponent, TodoItemPresenter: '' as AnyComponent,
StatusTableView: '' as AnyComponent, StatusTableView: '' as AnyComponent,
TaskHeader: '' as AnyComponent, TaskHeader: '' as AnyComponent,
Dashboard: '' as AnyComponent Dashboard: '' as AnyComponent,
StateRefPresenter: '' as AnyComponent,
DoneStateRefPresenter: '' as AnyComponent
}, },
space: { space: {
TasksPublic: '' as Ref<Space> TasksPublic: '' as Ref<Space>

View File

@ -73,7 +73,7 @@ import {
trackerId, trackerId,
WorkDayLength WorkDayLength
} from '@hcengineering/tracker' } from '@hcengineering/tracker'
import { KeyBinding } from '@hcengineering/view' import { KeyBinding, ViewOptionsModel } from '@hcengineering/view'
import tracker from './plugin' import tracker from './plugin'
import presentation from '@hcengineering/model-presentation' import presentation from '@hcengineering/model-presentation'
@ -467,9 +467,31 @@ export function createModel (builder: Builder): void {
TTypeReportedTime TTypeReportedTime
) )
const issuesOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'project', 'sprint'],
orderBy: [
['status', SortingOrder.Ascending],
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['dueDate', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
],
other: [
{
key: 'shouldShowSubIssues',
type: 'toggle',
defaultValue: false,
actionTartget: 'query',
action: tracker.function.SubIssueQuery,
label: tracker.string.SubIssues
}
]
}
builder.createDoc(view.class.Viewlet, core.space.Model, { builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: tracker.class.Issue, attachTo: tracker.class.Issue,
descriptor: tracker.viewlet.List, descriptor: view.viewlet.List,
viewOptions: issuesOptions,
config: [ config: [
{ {
key: '', key: '',
@ -484,7 +506,7 @@ export function createModel (builder: Builder): void {
}, },
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } }, { key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} }, { key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
{ key: '', presenter: tracker.component.GrowPresenter, props: { type: 'grow' } }, { key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{ key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } }, { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
{ {
key: '', key: '',
@ -530,11 +552,21 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, { builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: tracker.class.IssueTemplate, attachTo: tracker.class.IssueTemplate,
descriptor: tracker.viewlet.List, descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['assignee', 'priority', 'project', 'sprint'],
orderBy: [
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['dueDate', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
],
other: []
},
config: [ config: [
// { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } }, // { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } },
{ key: '', presenter: tracker.component.IssueTemplatePresenter, props: { type: 'issue', shouldUseMargin: true } }, { key: '', presenter: tracker.component.IssueTemplatePresenter, props: { type: 'issue', shouldUseMargin: true } },
{ key: '', presenter: tracker.component.GrowPresenter, props: { type: 'grow' } }, { key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
// { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } }, // { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
{ {
key: '', key: '',
@ -556,20 +588,10 @@ export function createModel (builder: Builder): void {
] ]
}) })
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: tracker.string.List,
icon: view.icon.Table,
component: tracker.component.ListView
},
tracker.viewlet.List
)
builder.createDoc(view.class.Viewlet, core.space.Model, { builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: tracker.class.Issue, attachTo: tracker.class.Issue,
descriptor: tracker.viewlet.Kanban, descriptor: tracker.viewlet.Kanban,
viewOptions: issuesOptions,
config: [] config: []
}) })
@ -657,11 +679,11 @@ export function createModel (builder: Builder): void {
const sprintsId = 'sprints' const sprintsId = 'sprints'
const templatesId = 'templates' const templatesId = 'templates'
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.IssuePresenter presenter: tracker.component.IssuePresenter
}) })
builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.IssueTemplatePresenter presenter: tracker.component.IssueTemplatePresenter
}) })
@ -669,7 +691,7 @@ export function createModel (builder: Builder): void {
presenter: tracker.component.IssuePreview presenter: tracker.component.IssuePreview
}) })
builder.mixin(tracker.class.TimeSpendReport, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.TimeSpendReport, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.TimeSpendReport presenter: tracker.component.TimeSpendReport
}) })
@ -677,7 +699,23 @@ export function createModel (builder: Builder): void {
titleProvider: tracker.function.IssueTitleProvider titleProvider: tracker.function.IssueTitleProvider
}) })
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ListHeaderExtra, {
presenters: [tracker.component.IssueStatistics]
})
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.SortFuncs, {
func: tracker.function.IssueStatusSort
})
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.SortFuncs, {
func: tracker.function.IssuePrioritySort
})
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.SortFuncs, {
func: tracker.function.SprintSort
})
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.PriorityPresenter presenter: tracker.component.PriorityPresenter
}) })
@ -685,33 +723,40 @@ export function createModel (builder: Builder): void {
component: view.component.ValueFilter component: view.component.ValueFilter
}) })
builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.StatusPresenter presenter: tracker.component.StatusPresenter
}) })
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.IssueStatus, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.StatusRefPresenter
})
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.PriorityRefPresenter
})
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.ProjectTitlePresenter presenter: tracker.component.ProjectTitlePresenter
}) })
builder.mixin(tracker.class.Team, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.Team, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.TeamPresenter presenter: tracker.component.TeamPresenter
}) })
classPresenter( classPresenter(builder, tracker.class.Project, tracker.component.ProjectSelector, tracker.component.ProjectSelector)
builder,
tracker.class.Project,
tracker.component.ProjectTitlePresenter,
tracker.component.ProjectSelector
)
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AttributeEditor, { builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: tracker.component.ProjectSelector inlineEditor: tracker.component.ProjectSelector
}) })
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.SprintTitlePresenter presenter: tracker.component.SprintTitlePresenter
}) })
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.SprintRefPresenter
})
builder.mixin(tracker.class.Issue, core.class.Class, setting.mixin.Editable, { builder.mixin(tracker.class.Issue, core.class.Class, setting.mixin.Editable, {
value: true value: true
}) })

View File

@ -41,7 +41,8 @@ export default mergeIds(trackerId, tracker, {
// Required to pass build without errorsF // Required to pass build without errorsF
Nope: '' as AnyComponent, Nope: '' as AnyComponent,
SprintSelector: '' as AnyComponent, SprintSelector: '' as AnyComponent,
SubIssuesSelector: '' as AnyComponent SubIssuesSelector: '' as AnyComponent,
IssueStatistics: '' as AnyComponent
}, },
app: { app: {
Tracker: '' as Ref<Application> Tracker: '' as Ref<Application>

View File

@ -29,6 +29,7 @@ import type {
AttributePresenter, AttributePresenter,
BuildModelKey, BuildModelKey,
ClassFilters, ClassFilters,
ClassSortFuncs,
CollectionEditor, CollectionEditor,
CollectionPresenter, CollectionPresenter,
Filter, Filter,
@ -38,13 +39,16 @@ import type {
KeyBinding, KeyBinding,
KeyFilter, KeyFilter,
LinkPresenter, LinkPresenter,
ListHeaderExtra,
ListItemPresenter,
ObjectEditor, ObjectEditor,
ObjectEditorHeader, ObjectEditorHeader,
ObjectFactory, ObjectFactory,
ObjectPresenter,
ObjectTitle, ObjectTitle,
ObjectValidator, ObjectValidator,
PreviewPresenter, PreviewPresenter,
ListItemPresenter, SortFunc,
SpaceHeader, SpaceHeader,
SpaceName, SpaceName,
ViewAction, ViewAction,
@ -52,7 +56,8 @@ import type {
ViewContext, ViewContext,
Viewlet, Viewlet,
ViewletDescriptor, ViewletDescriptor,
ViewletPreference ViewletPreference,
ViewOptionsModel
} from '@hcengineering/view' } from '@hcengineering/view'
import view from './plugin' import view from './plugin'
@ -134,6 +139,11 @@ export class TAttributePresenter extends TClass implements AttributePresenter {
presenter!: AnyComponent presenter!: AnyComponent
} }
@Mixin(view.mixin.ObjectPresenter, core.class.Class)
export class TObjectPresenter extends TClass implements ObjectPresenter {
presenter!: AnyComponent
}
@Mixin(view.mixin.ListItemPresenter, core.class.Class) @Mixin(view.mixin.ListItemPresenter, core.class.Class)
export class TListItemPresenter extends TClass implements ListItemPresenter { export class TListItemPresenter extends TClass implements ListItemPresenter {
presenter!: AnyComponent presenter!: AnyComponent
@ -176,6 +186,16 @@ export class TObjectTitle extends TClass implements ObjectTitle {
titleProvider!: Resource<<T extends Doc>(client: Client, ref: Ref<T>) => Promise<string>> titleProvider!: Resource<<T extends Doc>(client: Client, ref: Ref<T>) => Promise<string>>
} }
@Mixin(view.mixin.ListHeaderExtra, core.class.Class)
export class TListHeaderExtra extends TClass implements ListHeaderExtra {
presenters!: AnyComponent[]
}
@Mixin(view.mixin.SortFuncs, core.class.Class)
export class TSortFuncs extends TClass implements ClassSortFuncs {
func!: SortFunc
}
@Model(view.class.ViewletPreference, preference.class.Preference) @Model(view.class.ViewletPreference, preference.class.Preference)
export class TViewletPreference extends TPreference implements ViewletPreference { export class TViewletPreference extends TPreference implements ViewletPreference {
attachedTo!: Ref<Viewlet> attachedTo!: Ref<Viewlet>
@ -190,11 +210,12 @@ export class TViewletDescriptor extends TDoc implements ViewletDescriptor {
@Model(view.class.Viewlet, core.class.Doc, DOMAIN_MODEL) @Model(view.class.Viewlet, core.class.Doc, DOMAIN_MODEL)
export class TViewlet extends TDoc implements Viewlet { export class TViewlet extends TDoc implements Viewlet {
attachTo!: Ref<Class<Space>> attachTo!: Ref<Class<Doc>>
descriptor!: Ref<ViewletDescriptor> descriptor!: Ref<ViewletDescriptor>
open!: AnyComponent open!: AnyComponent
config!: (BuildModelKey | string)[] config!: (BuildModelKey | string)[]
hiddenKeys?: string[] hiddenKeys?: string[]
viewOptions?: ViewOptionsModel
} }
@Model(view.class.Action, core.class.Doc, DOMAIN_MODEL) @Model(view.class.Action, core.class.Doc, DOMAIN_MODEL)
@ -279,6 +300,9 @@ export function createModel (builder: Builder): void {
TCollectionEditor, TCollectionEditor,
TCollectionPresenter, TCollectionPresenter,
TObjectEditor, TObjectEditor,
TObjectPresenter,
TSortFuncs,
TListHeaderExtra,
TViewletPreference, TViewletPreference,
TViewletDescriptor, TViewletDescriptor,
TViewlet, TViewlet,
@ -330,7 +354,14 @@ export function createModel (builder: Builder): void {
classPresenter(builder, core.class.TypeTimestamp, view.component.TimestampPresenter) classPresenter(builder, core.class.TypeTimestamp, view.component.TimestampPresenter)
classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor) classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor)
classPresenter(builder, core.class.Space, view.component.ObjectPresenter) classPresenter(builder, core.class.Space, view.component.ObjectPresenter)
classPresenter(builder, core.class.Class, view.component.ClassPresenter) classPresenter(builder, core.class.Class, view.component.ClassRefPresenter)
builder.mixin(core.class.Space, core.class.Class, view.mixin.ObjectPresenter, {
presenter: view.component.SpacePresenter
})
builder.mixin(core.class.Class, core.class.Class, view.mixin.ObjectPresenter, {
presenter: view.component.ClassPresenter
})
classPresenter(builder, core.class.TypeRelatedDocument, view.component.ObjectPresenter) classPresenter(builder, core.class.TypeRelatedDocument, view.component.ObjectPresenter)
@ -376,6 +407,17 @@ export function createModel (builder: Builder): void {
view.viewlet.Table view.viewlet.Table
) )
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: view.string.Table,
icon: view.icon.Table,
component: view.component.ListView
},
view.viewlet.List
)
createAction( createAction(
builder, builder,
{ {

View File

@ -58,10 +58,13 @@ export default mergeIds(viewId, view, {
YoutubePresenter: '' as AnyComponent, YoutubePresenter: '' as AnyComponent,
GithubPresenter: '' as AnyComponent, GithubPresenter: '' as AnyComponent,
ClassPresenter: '' as AnyComponent, ClassPresenter: '' as AnyComponent,
ClassRefPresenter: '' as AnyComponent,
EnumEditor: '' as AnyComponent, EnumEditor: '' as AnyComponent,
HTMLEditor: '' as AnyComponent, HTMLEditor: '' as AnyComponent,
MarkupEditor: '' as AnyComponent, MarkupEditor: '' as AnyComponent,
MarkupEditorPopup: '' as AnyComponent MarkupEditorPopup: '' as AnyComponent,
ListView: '' as AnyComponent,
GrowPresenter: '' as AnyComponent
}, },
string: { string: {
Table: '' as IntlString, Table: '' as IntlString,
@ -84,7 +87,8 @@ export default mergeIds(viewId, view, {
General: '' as IntlString, General: '' as IntlString,
Navigation: '' as IntlString, Navigation: '' as IntlString,
Editor: '' as IntlString, Editor: '' as IntlString,
MarkdownFormatting: '' as IntlString MarkdownFormatting: '' as IntlString,
List: '' as IntlString
}, },
function: { function: {
FilterObjectInResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>, FilterObjectInResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>,

View File

@ -48,7 +48,7 @@ export class TSpaceView extends TClass implements SpaceView {
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TApplication, TSpaceView, THiddenApplication) builder.createModel(TApplication, TSpaceView, THiddenApplication)
builder.mixin(workbench.class.Application, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(workbench.class.Application, core.class.Class, view.mixin.ObjectPresenter, {
presenter: workbench.component.ApplicationPresenter presenter: workbench.component.ApplicationPresenter
}) })
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { AttachedDoc, Class, Doc, DocumentQuery, DocumentUpdate, FindOptions, Ref } from '@hcengineering/core' import { Class, Doc, DocumentQuery, DocumentUpdate, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { getPlatformColor, ScrollBox, Scroller } from '@hcengineering/ui' import { getPlatformColor, ScrollBox, Scroller } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
@ -22,14 +22,13 @@
import KanbanRow from './KanbanRow.svelte' import KanbanRow from './KanbanRow.svelte'
export let _class: Ref<Class<Item>> export let _class: Ref<Class<Item>>
export let search: string
export let options: FindOptions<Item> | undefined = undefined export let options: FindOptions<Item> | undefined = undefined
export let states: TypeState[] = [] export let states: TypeState[] = []
export let query: DocumentQuery<Item> = {} export let query: DocumentQuery<Item> = {}
export let fieldName: string export let fieldName: string
export let rankFieldName: string | undefined
export let selection: number | undefined = undefined export let selection: number | undefined = undefined
export let checked: Doc[] = [] export let checked: Doc[] = []
export let dontUpdateRank: boolean = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -38,15 +37,13 @@
const objsQ = createQuery() const objsQ = createQuery()
$: objsQ.query( $: objsQ.query(
_class, _class,
{ query,
...query,
...(search !== '' ? { $search: search } : {})
},
(result) => { (result) => {
objects = result objects = result
dispatch('content', objects) dispatch('content', objects)
}, },
{ {
sort: { rank: SortingOrder.Ascending },
...options ...options
} }
) )
@ -57,10 +54,6 @@
dragItem?: Item // required for svelte to properly recalculate state. dragItem?: Item // required for svelte to properly recalculate state.
): ExtItem[] { ): ExtItem[] {
const stateCards = objects.filter((it) => (it as any)[fieldName] === state._id) const stateCards = objects.filter((it) => (it as any)[fieldName] === state._id)
if (rankFieldName !== undefined) {
const sortField = rankFieldName
stateCards.sort((a, b) => (a as any)[sortField]?.localeCompare((b as any)[sortField]))
}
return stateCards.map((it, idx, arr) => ({ return stateCards.map((it, idx, arr) => ({
it, it,
prev: arr[idx - 1], prev: arr[idx - 1],
@ -69,23 +62,6 @@
})) }))
} }
async function updateItem (item: Item, update: DocumentUpdate<Item>) {
if (client.getHierarchy().isDerived(_class, core.class.AttachedDoc)) {
const adoc: AttachedDoc = item as Doc as AttachedDoc
await client.updateCollection(
_class,
adoc.space,
adoc._id as Ref<Doc> as Ref<AttachedDoc>,
adoc.attachedTo,
adoc.attachedToClass,
adoc.collection,
update
)
} else {
await client.updateDoc(item._class, item.space, item._id, update)
}
}
async function move (state: StateType) { async function move (state: StateType) {
if (dragCard === undefined) { if (dragCard === undefined) {
return return
@ -99,15 +75,15 @@
} }
} }
if (rankFieldName !== undefined && dragCardInitialRank !== (dragCard as any)[rankFieldName]) { if (!dontUpdateRank && dragCardInitialRank !== dragCard.rank) {
const dragCardRank = (dragCard as any)[rankFieldName] const dragCardRank = dragCard.rank
updates = { updates = {
...updates, ...updates,
[rankFieldName]: dragCardRank rank: dragCardRank
} }
} }
if (Object.keys(updates).length > 0) { if (Object.keys(updates).length > 0) {
await updateItem(dragCard, updates) await client.update(dragCard, updates)
} }
dragCard = undefined dragCard = undefined
} }
@ -125,7 +101,7 @@
if (dragCard === undefined) { if (dragCard === undefined) {
return return
} }
await updateItem(dragCard, query) await client.update(dragCard, query)
} }
function doCalcRank ( function doCalcRank (
object: { prev?: Item; it: Item; next?: Item }, object: { prev?: Item; it: Item; next?: Item },
@ -146,26 +122,28 @@
if (card !== undefined && card[fieldName] !== state._id) { if (card !== undefined && card[fieldName] !== state._id) {
card[fieldName] = state._id card[fieldName] = state._id
const objs = getStateObjects(objects, state) const objs = getStateObjects(objects, state)
if (rankFieldName !== undefined) card[rankFieldName] = calcRank(objs[objs.length - 1]?.it, undefined) if (!dontUpdateRank) {
card.rank = calcRank(objs[objs.length - 1]?.it, undefined)
}
} }
} }
function cardDragOver (evt: CardDragEvent, object: ExtItem): void { function cardDragOver (evt: CardDragEvent, object: ExtItem): void {
if (dragCard !== undefined) { if (dragCard !== undefined) {
;(dragCard as any)[fieldName] = (dragCard as any)[fieldName] ;(dragCard as any)[fieldName] = (dragCard as any)[fieldName]
if (rankFieldName !== undefined) { if (!dontUpdateRank) {
;(dragCard as any)[rankFieldName] = doCalcRank(object, evt) dragCard.rank = doCalcRank(object, evt)
} }
} }
} }
function cardDrop (evt: CardDragEvent, object: ExtItem): void { function cardDrop (evt: CardDragEvent, object: ExtItem): void {
if (dragCard !== undefined && rankFieldName !== undefined) { if (!dontUpdateRank && dragCard !== undefined) {
;(dragCard as any)[rankFieldName] = doCalcRank(object, evt) dragCard.rank = doCalcRank(object, evt)
} }
isDragging = false isDragging = false
} }
function onDragStart (object: ExtItem, state: TypeState): void { function onDragStart (object: ExtItem, state: TypeState): void {
dragCardInitialState = state._id dragCardInitialState = state._id
dragCardInitialRank = rankFieldName === undefined ? undefined : (object.it as any)[rankFieldName] dragCardInitialRank = object.it.rank
dragCard = object.it dragCard = object.it
isDragging = true isDragging = true
dispatch('obj-focus', object.it) dispatch('obj-focus', object.it)

View File

@ -27,6 +27,7 @@
"Add": "Add", "Add": "Add",
"Edit": "Edit", "Edit": "Edit",
"SelectAvatar": "Select avatar", "SelectAvatar": "Select avatar",
"GravatarsManaged": "Gravatars are managed through" "GravatarsManaged": "Gravatars are managed through",
"InltPropsValue": "{value}"
} }
} }

View File

@ -27,6 +27,7 @@
"Add": "Добавить", "Add": "Добавить",
"Edit": "Редактировать", "Edit": "Редактировать",
"SelectAvatar": "Выбрать аватар", "SelectAvatar": "Выбрать аватар",
"GravatarsManaged": "Граватары управляются через" "GravatarsManaged": "Граватары управляются через",
"InltPropsValue": "{value}"
} }
} }

View File

@ -56,7 +56,8 @@ export default plugin(presentationId, {
Add: '' as IntlString, Add: '' as IntlString,
Edit: '' as IntlString, Edit: '' as IntlString,
SelectAvatar: '' as IntlString, SelectAvatar: '' as IntlString,
GravatarsManaged: '' as IntlString GravatarsManaged: '' as IntlString,
InltPropsValue: '' as IntlString
}, },
metadata: { metadata: {
RequiredVersion: '' as Metadata<string>, RequiredVersion: '' as Metadata<string>,

View File

@ -200,7 +200,7 @@ export async function copyTextToClipboard (text: string): Promise<void> {
/** /**
* @public * @public
*/ */
export type AttributeCategory = 'attribute' | 'inplace' | 'collection' | 'array' export type AttributeCategory = 'object' | 'attribute' | 'inplace' | 'collection' | 'array'
/** /**
* @public * @public
@ -215,7 +215,7 @@ export function getAttributePresenterClass (
attribute: AnyAttribute attribute: AnyAttribute
): { attrClass: Ref<Class<Doc>>, category: AttributeCategory } { ): { attrClass: Ref<Class<Doc>>, category: AttributeCategory } {
let attrClass = attribute.type._class let attrClass = attribute.type._class
let category: AttributeCategory = 'attribute' let category: AttributeCategory = 'object'
if (hierarchy.isDerived(attrClass, core.class.RefTo)) { if (hierarchy.isDerived(attrClass, core.class.RefTo)) {
attrClass = (attribute.type as RefTo<Doc>).to attrClass = (attribute.type as RefTo<Doc>).to
category = 'attribute' category = 'attribute'

View File

@ -32,7 +32,7 @@
let time: string = '' let time: string = ''
async function formatTime (now: number) { async function formatTime (now: number, value: number) {
let passed = now - value let passed = now - value
if (passed < 0) passed = 0 if (passed < 0) passed = 0
if (passed < HOUR) { if (passed < HOUR) {
@ -48,7 +48,7 @@
} }
} }
$: formatTime($ticker) $: formatTime($ticker, value)
$: tooltipValue = new Date(value).toLocaleString('default', { $: tooltipValue = new Date(value).toLocaleString('default', {
minute: '2-digit', minute: '2-digit',

View File

@ -15,7 +15,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import board, { Card } from '@hcengineering/board' import board, { Card } from '@hcengineering/board'
import { Class, Doc, FindOptions, Ref, SortingOrder, WithLookup } from '@hcengineering/core' import { Class, Doc, DocumentQuery, FindOptions, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { Kanban as KanbanUI } from '@hcengineering/kanban' import { Kanban as KanbanUI } from '@hcengineering/kanban'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import type { Kanban, SpaceWithStates, State } from '@hcengineering/task' import type { Kanban, SpaceWithStates, State } from '@hcengineering/task'
@ -37,7 +37,7 @@
export let _class: Ref<Class<Card>> export let _class: Ref<Class<Card>>
export let space: Ref<SpaceWithStates> export let space: Ref<SpaceWithStates>
export let search: string export let query: DocumentQuery<Card>
export let options: FindOptions<Card> | undefined export let options: FindOptions<Card> | undefined
let kanban: Kanban let kanban: Kanban
@ -95,6 +95,8 @@
showPopup(ContextMenu, { object }, getEventPositionElement(ev)) showPopup(ContextMenu, { object }, getEventPositionElement(ev))
} }
$: resultQuery = { ...query, doneState: null, isArchived: { $nin: [true] }, space }
</script> </script>
<ActionContext <ActionContext
@ -105,12 +107,10 @@
<KanbanUI <KanbanUI
bind:this={kanbanUI} bind:this={kanbanUI}
{_class} {_class}
{search}
{options} {options}
query={{ doneState: null, isArchived: { $nin: [true] }, space }} query={resultQuery}
{states} {states}
fieldName={'state'} fieldName={'state'}
rankFieldName={'rank'}
on:content={(evt) => { on:content={(evt) => {
listProvider.update(evt.detail) listProvider.update(evt.detail)
}} }}

View File

@ -23,7 +23,7 @@
{_class} {_class}
config={[ config={[
'title', 'title',
'$lookup.state', 'state',
{ {
key: '', key: '',
presenter: tags.component.TagsPresenter, presenter: tags.component.TagsPresenter,

View File

@ -40,7 +40,6 @@
export let options: FindOptions<Event> | undefined = undefined export let options: FindOptions<Event> | undefined = undefined
export let baseMenuClass: Ref<Class<Event>> | undefined = undefined export let baseMenuClass: Ref<Class<Event>> | undefined = undefined
export let config: (string | BuildModelKey)[] export let config: (string | BuildModelKey)[]
export let search: string = ''
export let createComponent: AnyComponent | undefined = undefined export let createComponent: AnyComponent | undefined = undefined
const mondayStart = true const mondayStart = true
@ -49,10 +48,6 @@
let currentDate: Date = new Date() let currentDate: Date = new Date()
let selectedDate: Date = new Date() let selectedDate: Date = new Date()
let resultQuery: DocumentQuery<Event>
$: spaceOpt = space ? { space } : {}
$: resultQuery = search === '' ? { ...query, ...spaceOpt } : { ...query, $search: search, ...spaceOpt }
let objects: Event[] = [] let objects: Event[] = []
const q = createQuery() const q = createQuery()
@ -67,7 +62,7 @@
{ sort: { date: SortingOrder.Ascending }, ...options } { sort: { date: SortingOrder.Ascending }, ...options }
) )
} }
$: update(_class, resultQuery, options) $: update(_class, query, options)
function areDatesLess (firstDate: Date, secondDate: Date): boolean { function areDatesLess (firstDate: Date, secondDate: Date): boolean {
return ( return (
@ -256,7 +251,7 @@
{today} {today}
{selected} {selected}
{wrongMonth} {wrongMonth}
query={resultQuery} {query}
/> />
</svelte:fragment> </svelte:fragment>
</YearCalendar> </YearCalendar>
@ -276,7 +271,7 @@
{today} {today}
{selected} {selected}
{wrongMonth} {wrongMonth}
query={resultQuery} {query}
on:select={(e) => { on:select={(e) => {
currentDate = e.detail currentDate = e.detail
if (areDatesEqual(selectedDate, currentDate)) { if (areDatesEqual(selectedDate, currentDate)) {

View File

@ -72,6 +72,7 @@
"Birthday": "Birthday", "Birthday": "Birthday",
"UseImage": "Upload an image", "UseImage": "Upload an image",
"UseGravatar": "Use Gravatar", "UseGravatar": "Use Gravatar",
"UseColor": "Use color" "UseColor": "Use color",
"NotSpecified": "Not specified"
} }
} }

View File

@ -72,6 +72,7 @@
"Birthday": "День рождения", "Birthday": "День рождения",
"UseImage": "Загрузить фото", "UseImage": "Загрузить фото",
"UseGravatar": "Использовать Gravatar", "UseGravatar": "Использовать Gravatar",
"UseColor": "Использовать цвет" "UseColor": "Использовать цвет",
"NotSpecified": "Не указан"
} }
} }

View File

@ -0,0 +1,33 @@
<!--
// 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 { Contact } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import contact from '../plugin'
import ContactPresenter from './ContactPresenter.svelte'
export let value: Ref<Contact>
export let isInteractive = true
let doc: Contact | undefined
const query = createQuery()
$: value && query.query(contact.class.Contact, { _id: value }, (res) => ([doc] = res), { limit: 1 })
</script>
{#if doc}
<ContactPresenter value={doc} {isInteractive} />
{/if}

View File

@ -2,21 +2,14 @@
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import { WithLookup } from '@hcengineering/core' import { WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import type { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
import { showPopup } from '@hcengineering/ui' import { showPopup } from '@hcengineering/ui'
import { PersonLabelTooltip } from '..'
import PersonPresenter from '../components/PersonPresenter.svelte' import PersonPresenter from '../components/PersonPresenter.svelte'
import EmployeePreviewPopup from './EmployeePreviewPopup.svelte' import EmployeePreviewPopup from './EmployeePreviewPopup.svelte'
import EmployeeStatusPresenter from './EmployeeStatusPresenter.svelte' import EmployeeStatusPresenter from './EmployeeStatusPresenter.svelte'
export let value: WithLookup<Employee> | null | undefined export let value: WithLookup<Employee> | null | undefined
export let tooltipLabels: export let tooltipLabels: PersonLabelTooltip | undefined = undefined
| {
personLabel?: IntlString
placeholderLabel?: IntlString
component?: AnySvelteComponent | AnyComponent
props?: any
}
| undefined = undefined
export let shouldShowAvatar: boolean = true export let shouldShowAvatar: boolean = true
export let shouldShowName: boolean = true export let shouldShowName: boolean = true
export let shouldShowPlaceholder = false export let shouldShowPlaceholder = false
@ -25,6 +18,7 @@
export let isInteractive = true export let isInteractive = true
export let inline = false export let inline = false
export let disableClick = false export let disableClick = false
export let defaultName: IntlString | undefined = undefined
let container: HTMLElement let container: HTMLElement
@ -48,7 +42,7 @@
$: handlePersonEdit = onEmployeeEdit ?? onEdit $: handlePersonEdit = onEmployeeEdit ?? onEdit
</script> </script>
<div bind:this={container} class:over-underline={!inline}> <div bind:this={container}>
<PersonPresenter <PersonPresenter
{value} {value}
{tooltipLabels} {tooltipLabels}
@ -59,6 +53,7 @@
{shouldShowPlaceholder} {shouldShowPlaceholder}
{isInteractive} {isInteractive}
{inline} {inline}
{defaultName}
/> />
</div> </div>
{#if value?.$lookup?.statuses?.length} {#if value?.$lookup?.statuses?.length}

View File

@ -0,0 +1,39 @@
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { ButtonKind } from '@hcengineering/ui'
import { PersonLabelTooltip } from '..'
import contact from '../plugin'
import EmployeePresenter from './EmployeePresenter.svelte'
export let value: Ref<Employee> | null | undefined
export let kind: ButtonKind = 'link'
export let tooltipLabels: PersonLabelTooltip | undefined = undefined
let employee: Employee | undefined
const query = createQuery()
$: value && query.query(contact.class.Employee, { _id: value }, (res) => ([employee] = res), { limit: 1 })
function getValue (
employee: Employee | undefined,
value: Ref<Employee> | null | undefined
): Employee | null | undefined {
if (value === undefined || value === null) {
return value
}
return employee
}
</script>
<EmployeePresenter
value={getValue(employee, value)}
{tooltipLabels}
isInteractive={false}
shouldShowAvatar
shouldShowPlaceholder
defaultName={contact.string.NotSpecified}
shouldShowName={kind !== 'list'}
avatarSize={kind === 'list-header' ? 'small' : 'x-small'}
disableClick
/>

View File

@ -15,8 +15,10 @@
<script lang="ts"> <script lang="ts">
import { formatName, Person } from '@hcengineering/contact' import { formatName, Person } from '@hcengineering/contact'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { LabelAndProps } from '@hcengineering/ui'
import { PersonLabelTooltip } from '..'
import PersonContent from './PersonContent.svelte' import PersonContent from './PersonContent.svelte'
import type { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
export let value: Person | null | undefined export let value: Person | null | undefined
export let inline = false export let inline = false
@ -26,27 +28,40 @@
export let shouldShowName = true export let shouldShowName = true
export let shouldShowPlaceholder = false export let shouldShowPlaceholder = false
export let defaultName: IntlString | undefined = undefined export let defaultName: IntlString | undefined = undefined
export let tooltipLabels: export let tooltipLabels: PersonLabelTooltip | undefined = undefined
| {
personLabel?: IntlString
placeholderLabel?: IntlString
component?: AnySvelteComponent | AnyComponent
props?: any
}
| undefined = undefined
export let avatarSize: 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' = 'x-small' export let avatarSize: 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' = 'x-small'
export let onEdit: ((event: MouseEvent) => void) | undefined = undefined export let onEdit: ((event: MouseEvent) => void) | undefined = undefined
function getTooltip (
tooltipLabels: PersonLabelTooltip | undefined,
value: Person | null | undefined
): LabelAndProps | undefined {
if (!tooltipLabels) {
return !value
? undefined
: {
label: presentation.string.InltPropsValue,
props: { value: formatName(value.name) }
}
}
const component = value ? tooltipLabels.component : undefined
const label = value
? tooltipLabels.personLabel
? tooltipLabels.personLabel
: presentation.string.InltPropsValue
: undefined
const props = tooltipLabels.props ? tooltipLabels.props : value ? { value: formatName(value.name) } : undefined
return {
component,
label,
props
}
}
</script> </script>
{#if value || shouldShowPlaceholder} {#if value || shouldShowPlaceholder}
<PersonContent <PersonContent
showTooltip={tooltipLabels showTooltip={getTooltip(tooltipLabels, value)}
? {
label: value ? tooltipLabels.personLabel : undefined,
component: value ? tooltipLabels.component : undefined,
props: value && tooltipLabels.personLabel ? { value: formatName(value.name) } : tooltipLabels.props
}
: undefined}
{value} {value}
{inline} {inline}
{onEdit} {onEdit}

View File

@ -0,0 +1,59 @@
<!--
// 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, { Person } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { PersonLabelTooltip } from '..'
import PersonPresenter from './PersonPresenter.svelte'
export let value: Ref<Person> | null | undefined
export let inline = false
export let enlargedText = false
export let isInteractive = true
export let shouldShowAvatar = true
export let shouldShowName = true
export let shouldShowPlaceholder = false
export let defaultName: IntlString | undefined = undefined
export let tooltipLabels: PersonLabelTooltip | undefined = undefined
export let avatarSize: 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' = 'x-small'
export let onEdit: ((event: MouseEvent) => void) | undefined = undefined
let person: Person | undefined
const query = createQuery()
$: value && query.query(contact.class.Person, { _id: value }, (res) => ([person] = res), { limit: 1 })
function getValue (person: Person | undefined, value: Ref<Person> | null | undefined): Person | null | undefined {
if (value === undefined || value === null) {
return value
}
return person
}
</script>
<PersonPresenter
value={getValue(person, value)}
{onEdit}
{avatarSize}
{tooltipLabels}
{defaultName}
{shouldShowAvatar}
{shouldShowName}
{shouldShowPlaceholder}
{enlargedText}
{isInteractive}
{inline}
/>

View File

@ -17,9 +17,9 @@
import { Channel, Contact, Employee, formatName, getGravatarUrl } from '@hcengineering/contact' import { Channel, Contact, Employee, formatName, getGravatarUrl } from '@hcengineering/contact'
import { Class, Client, DocumentQuery, Ref, RelatedDocument, WithLookup } from '@hcengineering/core' import { Class, Client, DocumentQuery, Ref, RelatedDocument, WithLookup } from '@hcengineering/core'
import { leaveWorkspace } from '@hcengineering/login-resources' import { leaveWorkspace } from '@hcengineering/login-resources'
import { Resources } from '@hcengineering/platform' import { IntlString, Resources } from '@hcengineering/platform'
import { Avatar, getClient, MessageBox, ObjectSearchResult, UserInfo, getFileUrl } from '@hcengineering/presentation' import { Avatar, getClient, MessageBox, ObjectSearchResult, UserInfo, getFileUrl } from '@hcengineering/presentation'
import { showPopup } from '@hcengineering/ui' import { AnyComponent, AnySvelteComponent, showPopup } from '@hcengineering/ui'
import Channels from './components/Channels.svelte' import Channels from './components/Channels.svelte'
import ChannelsDropdown from './components/ChannelsDropdown.svelte' import ChannelsDropdown from './components/ChannelsDropdown.svelte'
import ChannelsEditor from './components/ChannelsEditor.svelte' import ChannelsEditor from './components/ChannelsEditor.svelte'
@ -48,7 +48,11 @@ import OrganizationPresenter from './components/OrganizationPresenter.svelte'
import PersonEditor from './components/PersonEditor.svelte' import PersonEditor from './components/PersonEditor.svelte'
import PersonPresenter from './components/PersonPresenter.svelte' import PersonPresenter from './components/PersonPresenter.svelte'
import SocialEditor from './components/SocialEditor.svelte' import SocialEditor from './components/SocialEditor.svelte'
import ContactRefPresenter from './components/ContactRefPresenter.svelte'
import PersonRefPresenter from './components/PersonRefPresenter.svelte'
import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte'
import contact from './plugin' import contact from './plugin'
import { employeeSort } from './utils'
export { export {
Channels, Channels,
@ -117,6 +121,13 @@ async function openChannelURL (doc: Channel): Promise<void> {
} }
} }
export interface PersonLabelTooltip {
personLabel?: IntlString
placeholderLabel?: IntlString
component?: AnySvelteComponent | AnyComponent
props?: any
}
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
actionImpl: { actionImpl: {
KickEmployee: kickEmployee, KickEmployee: kickEmployee,
@ -126,6 +137,8 @@ export default async (): Promise<Resources> => ({
PersonEditor, PersonEditor,
OrganizationEditor, OrganizationEditor,
ContactPresenter, ContactPresenter,
ContactRefPresenter,
PersonRefPresenter,
PersonPresenter, PersonPresenter,
OrganizationPresenter, OrganizationPresenter,
ChannelsPresenter, ChannelsPresenter,
@ -139,6 +152,7 @@ export default async (): Promise<Resources> => ({
Contacts, Contacts,
EmployeeAccountPresenter, EmployeeAccountPresenter,
EmployeePresenter, EmployeePresenter,
EmployeeRefPresenter,
Members, Members,
MemberPresenter, MemberPresenter,
EditMember, EditMember,
@ -164,6 +178,7 @@ export default async (): Promise<Resources> => ({
function: { function: {
GetFileUrl: getFileUrl, GetFileUrl: getFileUrl,
GetGravatarUrl: getGravatarUrl, GetGravatarUrl: getGravatarUrl,
GetColorUrl: (uri: string) => uri GetColorUrl: (uri: string) => uri,
EmployeeSort: employeeSort
} }
}) })

View File

@ -16,6 +16,7 @@
import contact, { contactId } from '@hcengineering/contact' import contact, { contactId } from '@hcengineering/contact'
import { IntlString, mergeIds } from '@hcengineering/platform' import { IntlString, mergeIds } from '@hcengineering/platform'
import { SortFunc } from '@hcengineering/view'
export default mergeIds(contactId, contact, { export default mergeIds(contactId, contact, {
string: { string: {
@ -61,6 +62,10 @@ export default mergeIds(contactId, contact, {
KickEmployeeDescr: '' as IntlString, KickEmployeeDescr: '' as IntlString,
Email: '' as IntlString, Email: '' as IntlString,
CreateEmployee: '' as IntlString, CreateEmployee: '' as IntlString,
Inactive: '' as IntlString Inactive: '' as IntlString,
NotSpecified: '' as IntlString
},
function: {
EmployeeSort: '' as SortFunc
} }
}) })

View File

@ -14,9 +14,9 @@
// limitations under the License. // limitations under the License.
// //
import contact, { ChannelProvider } from '@hcengineering/contact' import contact, { ChannelProvider, Employee, formatName } from '@hcengineering/contact'
import { Ref, Timestamp } from '@hcengineering/core' import { Ref, Timestamp } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
const client = getClient() const client = getClient()
const channelProviders = client.findAll(contact.class.ChannelProvider, {}) const channelProviders = client.findAll(contact.class.ChannelProvider, {})
@ -38,3 +38,35 @@ export function formatDate (dueDateMs: Timestamp): string {
minute: '2-digit' minute: '2-digit'
}) })
} }
export async function employeeSort (value: Array<Ref<Employee>>): Promise<Array<Ref<Employee>>> {
return await new Promise((resolve) => {
const query = createQuery(true)
query.query(contact.class.Employee, { _id: { $in: value } }, (res) => {
const employees = new Map(res.map((x) => [x._id, x]))
value.sort((a, b) => {
const employeeId1 = a as Ref<Employee> | null | undefined
const employeeId2 = b as Ref<Employee> | null | undefined
if (employeeId1 == null && employeeId2 != null) {
return 1
}
if (employeeId1 != null && employeeId2 == null) {
return -1
}
if (employeeId1 != null && employeeId2 != null) {
const name1 = formatName(employees.get(employeeId1)?.name ?? '')
const name2 = formatName(employees.get(employeeId2)?.name ?? '')
return name1.localeCompare(name2)
}
return 0
})
resolve(value)
query.unsubscribe()
})
})
}

View File

@ -0,0 +1,33 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 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 inventory, { Category } from '@hcengineering/inventory'
import { createQuery } from '@hcengineering/presentation'
import CategoryPresenter from './CategoryPresenter.svelte'
export let value: Ref<Category>
export let inline: boolean = false
let category: Category | undefined
const query = createQuery()
$: query.query(inventory.class.Category, { _id: value }, (res) => ([category] = res), { limit: 1 })
</script>
{#if category}
<CategoryPresenter {inline} value={category} />
{/if}

View File

@ -19,6 +19,7 @@ import { Resources } from '@hcengineering/platform'
import { showPopup } from '@hcengineering/ui' import { showPopup } from '@hcengineering/ui'
import Categories from './components/Categories.svelte' import Categories from './components/Categories.svelte'
import CategoryPresenter from './components/CategoryPresenter.svelte' import CategoryPresenter from './components/CategoryPresenter.svelte'
import CategoryRefPresenter from './components/CategoryRefPresenter.svelte'
import CreateCategory from './components/CreateCategory.svelte' import CreateCategory from './components/CreateCategory.svelte'
import EditProduct from './components/EditProduct.svelte' import EditProduct from './components/EditProduct.svelte'
import ProductPresenter from './components/ProductPresenter.svelte' import ProductPresenter from './components/ProductPresenter.svelte'
@ -37,6 +38,7 @@ export default async (): Promise<Resources> => ({
component: { component: {
Categories, Categories,
CategoryPresenter, CategoryPresenter,
CategoryRefPresenter,
ProductPresenter, ProductPresenter,
EditProduct, EditProduct,
Variants, Variants,

View File

@ -30,6 +30,7 @@
"Description": "Description", "Description": "Description",
"FullDescription": "Full description", "FullDescription": "Full description",
"FunnelPlaceholder": "The simple funnel", "FunnelPlaceholder": "The simple funnel",
"Members": "Members" "Members": "Members",
"UnAssign": "Unassign"
} }
} }

View File

@ -17,7 +17,7 @@
"Leads": "Сделки", "Leads": "Сделки",
"SelectCustomer": "Выбрать клиента", "SelectCustomer": "Выбрать клиента",
"Lead": "Сделка", "Lead": "Сделка",
"Assignee": "Назначена", "Assignee": "Исполнитель",
"Title": "Загаловок", "Title": "Загаловок",
"LeadPlaceholder": "Простая сделка", "LeadPlaceholder": "Простая сделка",
"ManageFunnelStatuses": "Управление статусами воронки", "ManageFunnelStatuses": "Управление статусами воронки",
@ -30,6 +30,7 @@
"Description": "Описание", "Description": "Описание",
"FullDescription": "Детальное описание", "FullDescription": "Детальное описание",
"FunnelPlaceholder": "Простая воронка", "FunnelPlaceholder": "Простая воронка",
"Members": "Пользователи" "Members": "Пользователи",
"UnAssign": "Отменить назначение"
} }
} }

View File

@ -14,11 +14,11 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Contact } from '@hcengineering/contact' import contact, { Contact, Employee } from '@hcengineering/contact'
import { AttachedData, generateId, Ref, SortingOrder, Space } from '@hcengineering/core' import { AttachedData, generateId, Ref, SortingOrder, Space } from '@hcengineering/core'
import type { Customer, Lead } from '@hcengineering/lead' import type { Customer, Funnel, Lead } from '@hcengineering/lead'
import { OK, Status } from '@hcengineering/platform' import { OK, Status } from '@hcengineering/platform'
import { Card, getClient, SpaceSelector, UserBox } from '@hcengineering/presentation' import { Card, createQuery, EmployeeBox, getClient, SpaceSelector, UserBox } from '@hcengineering/presentation'
import task, { calcRank } from '@hcengineering/task' import task, { calcRank } from '@hcengineering/task'
import { createFocusManager, EditBox, FocusHandler, Label, Status as StatusControl, Button } from '@hcengineering/ui' import { createFocusManager, EditBox, FocusHandler, Label, Status as StatusControl, Button } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
@ -41,14 +41,20 @@
return (preserveCustomer || customer === undefined) && title === '' return (preserveCustomer || customer === undefined) && title === ''
} }
$: client.findAll(lead.class.Funnel, {}).then((r) => { let funnels: Funnel[] = []
if (r.find((it) => it._id === _space) === undefined) { const funnelQuery = createQuery()
_space = r.shift()?._id as Ref<Space> funnelQuery.query(lead.class.Funnel, {}, (res) => (funnels = res))
$: {
if (funnels.find((it) => it._id === _space) === undefined) {
_space = funnels[0]?._id
} }
}) }
let assignee: Ref<Employee> | undefined = undefined
async function createLead () { async function createLead () {
const state = await client.findOne(task.class.State, { space: _space }) const state = await client.findOne(task.class.State, { space: _space }, { sort: { rank: SortingOrder.Ascending } })
if (state === undefined) { if (state === undefined) {
throw new Error('create application: state not found') throw new Error('create application: state not found')
} }
@ -145,6 +151,13 @@
</div> </div>
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">
<EmployeeBox
focusIndex={2}
label={lead.string.Assignee}
bind:value={assignee}
allowDeselect
titleDeselect={lead.string.UnAssign}
/>
{#if !preserveCustomer} {#if !preserveCustomer}
<UserBox <UserBox
focusIndex={2} focusIndex={2}

View File

@ -42,7 +42,7 @@
<Scroller horizontal> <Scroller horizontal>
<Table <Table
_class={lead.class.Lead} _class={lead.class.Lead}
config={['', '$lookup.state', '$lookup.doneState']} config={['', 'state', 'doneState']}
query={{ attachedTo: objectId }} query={{ attachedTo: objectId }}
{loadingProps} {loadingProps}
/> />
@ -50,7 +50,7 @@
{:else} {:else}
<Table <Table
_class={lead.class.Lead} _class={lead.class.Lead}
config={['', '$lookup.state', '$lookup.doneState']} config={['', 'state', 'doneState']}
query={{ attachedTo: objectId }} query={{ attachedTo: objectId }}
{loadingProps} {loadingProps}
/> />

View File

@ -21,8 +21,4 @@
export let value: Customer export let value: Customer
</script> </script>
<Table <Table _class={leads.class.Lead} config={['', 'state', 'doneState']} query={{ attachedTo: value._id }} />
_class={leads.class.Lead}
config={['', '$lookup.state', '$lookup.doneState']}
query={{ attachedTo: value._id }}
/>

View File

@ -41,7 +41,9 @@ export default mergeIds(leadId, lead, {
Description: '' as IntlString, Description: '' as IntlString,
FullDescription: '' as IntlString, FullDescription: '' as IntlString,
FunnelPlaceholder: '' as IntlString, FunnelPlaceholder: '' as IntlString,
Members: '' as IntlString Members: '' as IntlString,
Assignee: '' as IntlString,
UnAssign: '' as IntlString
}, },
component: { component: {
CreateCustomer: '' as AnyComponent, CreateCustomer: '' as AnyComponent,

View File

@ -35,8 +35,8 @@
'', '',
'$lookup.space.name', '$lookup.space.name',
'$lookup.space.$lookup.company', '$lookup.space.$lookup.company',
'$lookup.state', 'state',
'$lookup.doneState' 'doneState'
] ]
let wSection: number let wSection: number
</script> </script>

View File

@ -23,7 +23,7 @@
<Table <Table
_class={recruit.class.Applicant} _class={recruit.class.Applicant}
config={['', '$lookup.space.name', '$lookup.state', '$lookup.doneState']} config={['', '$lookup.space.name', 'state', 'doneState']}
query={{ attachedTo: value._id }} query={{ attachedTo: value._id }}
loadingProps={{ length: value.applications ?? 0 }} loadingProps={{ length: value.applications ?? 0 }}
/> />

View File

@ -35,7 +35,7 @@
<div class="popup-table"> <div class="popup-table">
<Table <Table
_class={recruit.class.Applicant} _class={recruit.class.Applicant}
config={['', '$lookup.attachedTo', '$lookup.state', '$lookup.doneState', 'modifiedOn']} config={['', 'attachedTo', 'state', 'doneState', 'modifiedOn']}
{options} {options}
query={{ ...(resultQuery ?? {}), space: value }} query={{ ...(resultQuery ?? {}), space: value }}
loadingProps={{ length: 0 }} loadingProps={{ length: 0 }}

View File

@ -88,16 +88,7 @@
<TableBrowser <TableBrowser
{_class} {_class}
config={[ config={['', 'attachedTo', 'assignee', 'state', 'doneState', 'attachments', 'comments', 'modifiedOn']}
'',
'$lookup.attachedTo',
'$lookup.assignee',
'$lookup.state',
'$lookup.doneState',
'attachments',
'comments',
'modifiedOn'
]}
query={resultQuery} query={resultQuery}
showNotification showNotification
/> />

View File

@ -18,11 +18,11 @@
import type { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task' import type { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task'
import task from '@hcengineering/task' import task from '@hcengineering/task'
import { BarDashboard, DashboardItem } from '@hcengineering/ui' import { BarDashboard, DashboardItem } from '@hcengineering/ui'
import { FilterBar } from '@hcengineering/view-resources'
import CreateFilter from './CreateFilter.svelte' import CreateFilter from './CreateFilter.svelte'
export let _class: Ref<Class<Task>> export let _class: Ref<Class<Task>>
export let space: Ref<SpaceWithStates> export let space: Ref<SpaceWithStates>
export let query: DocumentQuery<Task>
const client = getClient() const client = getClient()
const hieararchy = client.getHierarchy() const hieararchy = client.getHierarchy()
@ -85,15 +85,12 @@
const docQuery = createQuery() const docQuery = createQuery()
$: query = modified $: resultQuery = modified
? { ? {
space, _id: { $in: ids },
_id: { $in: ids } ...query
}
: { space }
let resultQuery = {
space
} }
: query
function updateDocs (_class: Ref<Class<Task>>, states: State[], query: DocumentQuery<Task>): void { function updateDocs (_class: Ref<Class<Task>>, states: State[], query: DocumentQuery<Task>): void {
if (states.length === 0) { if (states.length === 0) {
@ -146,7 +143,6 @@
</script> </script>
<CreateFilter bind:value={modified} /> <CreateFilter bind:value={modified} />
<FilterBar {_class} {query} on:change={(e) => (resultQuery = e.detail)} />
<div class="ml-10 mt-4"> <div class="ml-10 mt-4">
<BarDashboard {items} /> <BarDashboard {items} />

View File

@ -15,7 +15,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Class, DocumentQuery, FindOptions, Ref, SortingOrder } from '@hcengineering/core' import { Class, DocumentQuery, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task' import { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task'
import { TabList } from '@hcengineering/ui' import { TabList } from '@hcengineering/ui'
import type { TabItem } from '@hcengineering/ui' import type { TabItem } from '@hcengineering/ui'
@ -27,26 +27,26 @@
export let _class: Ref<Class<Task>> export let _class: Ref<Class<Task>>
export let space: Ref<SpaceWithStates> export let space: Ref<SpaceWithStates>
export let query: DocumentQuery<Task>
export let options: FindOptions<Task> | undefined export let options: FindOptions<Task> | undefined
export let config: string[] export let config: string[]
export let search: string
let doneStatusesView: boolean = false let doneStatusesView: boolean = false
let state: Ref<State> | undefined = undefined let state: Ref<State> | undefined = undefined
const selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>() const selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
$: resConfig = updateConfig(config) $: resConfig = updateConfig(config)
let query = {}
let doneStates: DoneState[] = [] let doneStates: DoneState[] = []
let itemsDS: TabItem[] = [] let itemsDS: TabItem[] = []
let selectedDS: string[] = [] let selectedDS: string[] = []
let withoutDone: boolean = false let withoutDone: boolean = false
let resultQuery: DocumentQuery<Task>
function updateConfig (config: string[]): string[] { function updateConfig (config: string[]): string[] {
if (state !== undefined) { if (state !== undefined) {
return config.filter((p) => p !== '$lookup.state') return config.filter((p) => p !== 'state')
} }
if (selectedDoneStates.size === 1) { if (selectedDoneStates.size === 1) {
return config.filter((p) => p !== '$lookup.doneState') return config.filter((p) => p !== 'doneState')
} }
return config return config
} }
@ -77,12 +77,11 @@
} }
) )
async function updateQuery (search: string, selectedDoneStates: Set<Ref<DoneState>>): Promise<void> { const client = getClient()
async function updateQuery (query: DocumentQuery<Task>, selectedDoneStates: Set<Ref<DoneState>>): Promise<void> {
resConfig = updateConfig(config) resConfig = updateConfig(config)
const result = {} as DocumentQuery<Task> const result = client.getHierarchy().clone(query)
if (search !== '') {
result.$search = search
}
result.space = space result.space = space
if (state) { if (state) {
result.state = state result.state = state
@ -94,7 +93,7 @@
} else if (withoutDone) { } else if (withoutDone) {
result.doneState = null result.doneState = null
} }
query = result resultQuery = result
} }
function doneStateClick (id: Ref<DoneState>): void { function doneStateClick (id: Ref<DoneState>): void {
@ -108,17 +107,17 @@
selectedDS = ['NoDoneState'] selectedDS = ['NoDoneState']
withoutDone = true withoutDone = true
} }
updateQuery(search, selectedDoneStates) updateQuery(query, selectedDoneStates)
} }
function noDoneClick (): void { function noDoneClick (): void {
withoutDone = true withoutDone = true
selectedDS = ['NoDoneState'] selectedDS = ['NoDoneState']
selectedDoneStates.clear() selectedDoneStates.clear()
updateQuery(search, selectedDoneStates) updateQuery(query, selectedDoneStates)
} }
$: updateQuery(search, selectedDoneStates) $: updateQuery(query, selectedDoneStates)
const handleSelect = (result: any) => { const handleSelect = (result: any) => {
if (result.type === 'select') { if (result.type === 'select') {
const res = result.detail const res = result.detail
@ -127,12 +126,12 @@
state = undefined state = undefined
withoutDone = false withoutDone = false
selectedDoneStates.clear() selectedDoneStates.clear()
updateQuery(search, selectedDoneStates) updateQuery(query, selectedDoneStates)
} else if (res.id === 'DoneStates') { } else if (res.id === 'DoneStates') {
doneStatusesView = true doneStatusesView = true
state = undefined state = undefined
selectedDoneStates.clear() selectedDoneStates.clear()
updateQuery(search, selectedDoneStates) updateQuery(query, selectedDoneStates)
} }
} }
} }
@ -158,11 +157,11 @@
{#if doneStatusesView} {#if doneStatusesView}
<TabList items={itemsDS} bind:selected={selectedDS} multiselect on:select={handleDoneSelect} size={'small'} /> <TabList items={itemsDS} bind:selected={selectedDS} multiselect on:select={handleDoneSelect} size={'small'} />
{:else} {:else}
<StatesBar bind:state {space} gap={'none'} on:change={() => updateQuery(search, selectedDoneStates)} /> <StatesBar bind:state {space} gap={'none'} on:change={() => updateQuery(query, selectedDoneStates)} />
{/if} {/if}
</div> </div>
<div class="statustableview-container"> <div class="statustableview-container">
<TableBrowser {_class} bind:query config={resConfig} {options} showNotification /> <TableBrowser {_class} bind:query={resultQuery} config={resConfig} {options} showNotification />
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Class, Doc, FindOptions, Ref, SortingOrder } from '@hcengineering/core' import { Class, Doc, DocumentQuery, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
import { Kanban as KanbanUI } from '@hcengineering/kanban' import { Kanban as KanbanUI } from '@hcengineering/kanban'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
@ -23,7 +23,6 @@
import { getEventPositionElement, showPopup } from '@hcengineering/ui' import { getEventPositionElement, showPopup } from '@hcengineering/ui'
import { import {
ActionContext, ActionContext,
FilterBar,
focusStore, focusStore,
ListSelectionProvider, ListSelectionProvider,
SelectDirection, SelectDirection,
@ -35,8 +34,7 @@
export let _class: Ref<Class<Task>> export let _class: Ref<Class<Task>>
export let space: Ref<SpaceWithStates> export let space: Ref<SpaceWithStates>
// export let open: AnyComponent export let query: DocumentQuery<Task>
export let search: string
export let options: FindOptions<Task> | undefined export let options: FindOptions<Task> | undefined
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
// export let config: string[] // export let config: string[]
@ -96,7 +94,8 @@
listProvider.updateSelection(evt.detail.docs, evt.detail.value) listProvider.updateSelection(evt.detail.docs, evt.detail.value)
} }
const onContextMenu = (evt: any) => showMenu(evt.detail.evt, evt.detail.objects) const onContextMenu = (evt: any) => showMenu(evt.detail.evt, evt.detail.objects)
let resultQuery = { doneState: null, space }
$: resultQuery = { ...query, doneState: null }
</script> </script>
{#await cardPresenter then presenter} {#await cardPresenter then presenter}
@ -105,22 +104,13 @@
mode: 'browser' mode: 'browser'
}} }}
/> />
<FilterBar
{_class}
query={{ doneState: null, space }}
on:change={(e) => {
resultQuery = e.detail
}}
/>
<KanbanUI <KanbanUI
bind:this={kanbanUI} bind:this={kanbanUI}
{_class} {_class}
{search}
{options} {options}
query={resultQuery} query={resultQuery}
{states} {states}
fieldName={'state'} fieldName={'state'}
rankFieldName={'rank'}
on:content={onContent} on:content={onContent}
on:obj-focus={onObjFocus} on:obj-focus={onObjFocus}
checked={$selectionStore ?? []} checked={$selectionStore ?? []}

View File

@ -0,0 +1,33 @@
<!--
// 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 { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import type { DoneState } from '@hcengineering/task'
import task from '@hcengineering/task'
import DoneStatePresenter from './DoneStatePresenter.svelte'
export let value: Ref<DoneState>
export let showTitle: boolean = true
let state: DoneState | undefined
const query = createQuery()
$: query.query(task.class.DoneState, { _id: value }, (res) => ([state] = res), { limit: 1 })
</script>
{#if state}
<DoneStatePresenter value={state} {showTitle} />
{/if}

View File

@ -0,0 +1,31 @@
<!--
// 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 { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import task, { State } from '@hcengineering/task'
import StatePresenter from './StatePresenter.svelte'
export let value: Ref<State>
let state: State | undefined
const query = createQuery()
$: query.query(task.class.State, { _id: value }, (res) => ([state] = res), { limit: 1 })
</script>
{#if state}
<StatePresenter value={state} />
{/if}

View File

@ -39,6 +39,8 @@ import TodoItemsPopup from './components/todos/TodoItemsPopup.svelte'
import Todos from './components/todos/Todos.svelte' import Todos from './components/todos/Todos.svelte'
import TodoStatePresenter from './components/todos/TodoStatePresenter.svelte' import TodoStatePresenter from './components/todos/TodoStatePresenter.svelte'
import Dashboard from './components/Dashboard.svelte' import Dashboard from './components/Dashboard.svelte'
import DoneStateRefPresenter from './components/state/DoneStateRefPresenter.svelte'
import StateRefPresenter from './components/state/StateRefPresenter.svelte'
export { default as AssigneePresenter } from './components/AssigneePresenter.svelte' export { default as AssigneePresenter } from './components/AssigneePresenter.svelte'
@ -70,6 +72,8 @@ export default async (): Promise<Resources> => ({
KanbanTemplateEditor, KanbanTemplateEditor,
KanbanTemplateSelector, KanbanTemplateSelector,
AssignedTasks, AssignedTasks,
DoneStateRefPresenter,
StateRefPresenter,
TodoItemsPopup TodoItemsPopup
}, },
actionImpl: { actionImpl: {

View File

@ -35,4 +35,4 @@
) )
</script> </script>
<IssuesView {query} title={tracker.string.ActiveIssues} /> <IssuesView {query} space={currentSpace} title={tracker.string.ActiveIssues} />

View File

@ -15,17 +15,16 @@
<script lang="ts"> <script lang="ts">
import contact, { Employee } from '@hcengineering/contact' import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core' import { Class, Doc, Ref } from '@hcengineering/core'
import { Issue, IssueTemplate } from '@hcengineering/tracker'
import { UsersPopup, getClient } from '@hcengineering/presentation'
import { AttributeModel } from '@hcengineering/view'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { getObjectPresenter } from '@hcengineering/view-resources'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { getClient, UsersPopup } from '@hcengineering/presentation'
import { Issue, IssueTemplate } from '@hcengineering/tracker'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources'
import tracker from '../../plugin' import tracker from '../../plugin'
export let value: Employee | null | undefined export let value: Employee | null | undefined
export let issueId: Ref<Issue> export let object: Issue | IssueTemplate
export let issueClass: Ref<Class<Issue | IssueTemplate>> = tracker.class.Issue
export let defaultClass: Ref<Class<Doc>> | undefined = undefined export let defaultClass: Ref<Class<Doc>> | undefined = undefined
export let isEditable: boolean = true export let isEditable: boolean = true
export let shouldShowLabel: boolean = false export let shouldShowLabel: boolean = false
@ -52,15 +51,9 @@
return return
} }
const currentIssue = await client.findOne(issueClass, { _id: issueId })
if (currentIssue === undefined) {
return
}
const newAssignee = result === null ? null : result._id const newAssignee = result === null ? null : result._id
await client.update(currentIssue, { assignee: newAssignee }) await client.update(object, { assignee: newAssignee })
} }
const handleAssigneeEditorOpened = async (event: MouseEvent) => { const handleAssigneeEditorOpened = async (event: MouseEvent) => {

View File

@ -32,4 +32,4 @@
) )
</script> </script>
<IssuesView {query} title={tracker.string.BacklogIssues} /> <IssuesView {query} space={currentSpace} title={tracker.string.BacklogIssues} />

View File

@ -23,4 +23,4 @@
$: query = { space: currentSpace } $: query = { space: currentSpace }
</script> </script>
<IssuesView {query} title={tracker.string.AllIssues} /> <IssuesView {query} space={currentSpace} title={tracker.string.AllIssues} />

View File

@ -1,23 +1,32 @@
<script lang="ts"> <script lang="ts">
import { DocumentQuery, WithLookup } from '@hcengineering/core' import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui' import { Component } from '@hcengineering/ui'
import { Viewlet } from '@hcengineering/view' import { Viewlet } from '@hcengineering/view'
import { Issue } from '@hcengineering/tracker' import tracker from '../../plugin'
import { viewOptionsStore } from '@hcengineering/view-resources' import CreateIssue from '../CreateIssue.svelte'
export let viewlet: WithLookup<Viewlet> export let viewlet: WithLookup<Viewlet>
export let query: DocumentQuery<Issue> = {} export let query: DocumentQuery<Issue> = {}
export let space: Ref<Space> | undefined
const createItemDialog = CreateIssue
const createItemLabel = tracker.string.AddIssueTooltip
</script> </script>
{#if viewlet?.$lookup?.descriptor?.component} {#if viewlet?.$lookup?.descriptor?.component}
<Component <Component
is={viewlet.$lookup.descriptor.component} is={viewlet.$lookup.descriptor.component}
props={{ props={{
_class: tracker.class.Issue,
config: viewlet.config, config: viewlet.config,
options: viewlet.options, options: viewlet.options,
createItemDialog,
createItemLabel,
viewlet, viewlet,
query, viewOptions: viewlet.viewOptions?.other,
viewOptions: $viewOptionsStore space,
query
}} }}
/> />
{/if} {/if}

View File

@ -1,355 +0,0 @@
<!--
// 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, { Employee } from '@hcengineering/contact'
import { Class, Doc, FindOptions, Ref, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import ui, {
ActionIcon,
Button,
CheckBox,
Component,
eventToHTMLElement,
ExpandCollapse,
getEventPositionElement,
IconAdd,
IconMoreH,
Label,
showPopup,
Spinner
} from '@hcengineering/ui'
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
import {
buildModel,
filterStore,
FixedColumn,
getObjectPresenter,
LoadingProps,
Menu
} from '@hcengineering/view-resources'
import { onDestroy } from 'svelte'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import { IssuesGroupByKeys, issuesGroupEditorMap, IssuesOrderByKeys, issuesSortOrderMap } from '../../utils'
import CreateIssue from '../CreateIssue.svelte'
import IssueStatistics from '../sprints/IssueStatistics.svelte'
import IssuesListItem from './IssuesListItem.svelte'
export let _class: Ref<Class<Doc>>
export let currentSpace: Ref<Team> | undefined = undefined
export let groupByKey: IssuesGroupByKeys | undefined = undefined
export let orderBy: IssuesOrderByKeys
export let statuses: WithLookup<IssueStatus>[]
export let employees: (WithLookup<Employee> | undefined)[] = []
export let categories: any[] = []
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let itemsConfig: (BuildModelKey | string)[]
export let selectedObjectIds: Doc[] = []
export let selectedRowIndex: number | undefined = undefined
export let groupedIssues: { [key: string | number | symbol]: Issue[] } = {}
export let loadingProps: LoadingProps | undefined = undefined
const dispatch = createEventDispatcher()
const client = getClient()
const objectRefs: HTMLElement[] = []
const baseOptions: FindOptions<Issue> = {
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team,
_id: {
subIssues: tracker.class.Issue
}
}
}
const categoryLimit: Record<any, number> = {}
const spaceQuery = createQuery()
const defaultLimit = 20
const autoFoldLimit = 20
const singleCategoryLimit = 200
const noCategory = '#no_category'
let currentTeam: Team | undefined
let personPresenter: AttributeModel
let isCollapsedMap: Record<any, boolean> = {}
let itemModels: AttributeModel[]
let isFilterUpdate = false
let groupedIssuesBeforeFilter = groupedIssues
const handleMenuOpened = async (event: MouseEvent, object: Doc, rowIndex: number) => {
event.preventDefault()
selectedRowIndex = rowIndex
if (!selectedObjectIdsSet.has(object._id)) {
onObjectChecked(combinedGroupedIssues, false)
selectedObjectIds = []
}
const items = selectedObjectIds.length > 0 ? selectedObjectIds : object
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(event), () => {
selectedRowIndex = undefined
})
}
export const onObjectChecked = (docs: Doc[], value: boolean) => {
dispatch('check', { docs, value })
}
const handleRowFocused = (object: Doc) => {
dispatch('row-focus', object)
}
const handleNewIssueAdded = (event: MouseEvent, category: any) => {
if (!currentSpace) {
return
}
showPopup(
CreateIssue,
{ space: currentSpace, ...(groupByKey ? { [groupByKey]: category } : {}) },
eventToHTMLElement(event)
)
}
function toCat (category: any): any {
return 'cat-' + (category ?? noCategory)
}
const handleCollapseCategory = (category: any) => {
isCollapsedMap[category] = !isCollapsedMap[category]
}
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
if (options?.limit && options?.limit > 0) {
return Math.min(options.limit, props.length)
}
return props.length
}
function limitGroup (
category: any,
groupes: { [key: string | number | symbol]: Issue[] },
categoryLimit: Record<any, number>
): Issue[] {
const issues = groupes[category] ?? []
const initialLimit = Object.keys(groupes).length === 1 ? singleCategoryLimit : defaultLimit
const limit = categoryLimit[toCat(category)] ?? initialLimit
return issues.slice(0, limit)
}
const getInitCollapseValue = (category: any) =>
categories.length === 1 ? false : (groupedIssues[category]?.length ?? 0) > autoFoldLimit
const unsubscribeFilter = filterStore.subscribe(() => (isFilterUpdate = true))
onDestroy(unsubscribeFilter)
$: {
if (isFilterUpdate && groupedIssuesBeforeFilter !== groupedIssues && groupByKey) {
isCollapsedMap = {}
categories.forEach((category) => (isCollapsedMap[toCat(category)] = getInitCollapseValue(category)))
isFilterUpdate = false
groupedIssuesBeforeFilter = groupedIssues
}
}
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
currentTeam = res.shift()
})
$: {
const exkeys = new Set(Object.keys(isCollapsedMap))
for (const c of categories) {
if (!exkeys.delete(toCat(c))) {
isCollapsedMap[toCat(c)] = getInitCollapseValue(c)
}
}
for (const k of exkeys) {
delete isCollapsedMap[k]
}
}
$: combinedGroupedIssues = Object.values(groupedIssues).flat(1)
$: options = { ...baseOptions, sort: { [orderBy]: issuesSortOrderMap[orderBy] } } as FindOptions<Issue>
$: headerComponent = groupByKey === undefined || groupByKey === 'assignee' ? null : issuesGroupEditorMap[groupByKey]
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
$: objectRefs.length = combinedGroupedIssues.length
$: getObjectPresenter(client, contact.class.Person, { key: '' }).then((p) => {
personPresenter = p
})
$: buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }).then((res) => (itemModels = res))
</script>
<div class="issueslist-container">
{#each categories as category}
{@const items = groupedIssues[category] ?? []}
{@const limited = limitGroup(category, groupedIssues, categoryLimit) ?? []}
{#if headerComponent || groupByKey === 'assignee' || category === undefined}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(toCat(category))}>
<div class="flex-row-center gap-2 clear-mins">
<FixedColumn key={'issuelist_groupBy'} justify={'left'}>
{#if groupByKey === 'assignee' && personPresenter}
<svelte:component
this={personPresenter.presenter}
shouldShowLabel={true}
value={employees.find((x) => x?._id === category)}
defaultName={tracker.string.NoAssignee}
shouldShowPlaceholder={true}
isInteractive={false}
avatarSize={'small'}
enlargedText
{currentSpace}
/>
{:else if !groupByKey}
<span class="text-base fs-bold overflow-label content-accent-color pointer-events-none">
<Label label={tracker.string.NoGrouping} />
</span>
{:else if headerComponent}
<Component
is={headerComponent}
props={{
isEditable: false,
shouldShowLabel: true,
value: groupByKey ? { [groupByKey]: category } : {},
statuses: groupByKey === 'status' ? statuses : undefined,
issues: groupedIssues[category],
width: 'min-content',
kind: 'list-header',
enlargedText: true,
currentSpace
}}
/>
{/if}
</FixedColumn>
<FixedColumn key={'issuelist_statistics'} justify={'left'}>
<IssueStatistics issues={groupedIssues[category]} />
</FixedColumn>
{#if limited.length < items.length}
<div class="counter">
{limited.length}
<div class="text-xs mx-1">/</div>
{items.length}
</div>
<ActionIcon
size={'small'}
icon={IconMoreH}
label={ui.string.ShowMore}
action={() => {
categoryLimit[toCat(category)] = limited.length + 20
}}
/>
{:else}
<span class="counter">{items.length}</span>
{/if}
</div>
<Button
icon={IconAdd}
kind={'transparent'}
showTooltip={{ label: tracker.string.AddIssueTooltip }}
on:click={(event) => handleNewIssueAdded(event, category)}
/>
</div>
{/if}
<ExpandCollapse isExpanded={!isCollapsedMap[toCat(category)]} duration={400}>
{#if itemModels}
{#if groupedIssues[category]}
{#each limited as docObject (docObject._id)}
<IssuesListItem
bind:use={objectRefs[combinedGroupedIssues.findIndex((x) => x === docObject)]}
{docObject}
model={itemModels}
{groupByKey}
selected={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
checked={selectedObjectIdsSet.has(docObject._id)}
{statuses}
{currentTeam}
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
on:contextmenu={(event) =>
handleMenuOpened(
event,
docObject,
combinedGroupedIssues.findIndex((x) => x === docObject)
)}
on:focus={() => {}}
on:mouseover={() => handleRowFocused(docObject)}
/>
{/each}
{:else if loadingProps !== undefined}
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
<div class="listGrid row" class:fixed={rowIndex === selectedRowIndex}>
<div class="flex-center clear-mins h-full">
<div class="gridElement">
<CheckBox checked={false} />
<div class="ml-4">
<Spinner size="small" />
</div>
</div>
</div>
</div>
{/each}
{/if}
{/if}
</ExpandCollapse>
{/each}
</div>
<style lang="scss">
.issueslist-container {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: max-content;
min-width: auto;
min-height: auto;
}
.categoryHeader {
position: sticky;
top: 0;
padding: 0 0.75rem 0 2.25rem;
height: 3rem;
min-height: 3rem;
min-width: 0;
background: var(--header-bg-color);
z-index: 5;
}
.row:not(:last-child) {
border-bottom: 1px solid var(--accent-bg-color);
}
.counter {
display: flex;
align-items: center;
flex-wrap: nowrap;
flex-shrink: 0;
margin-left: 1rem;
padding: 0.25rem 0.5rem;
min-width: 1.325rem;
text-align: center;
font-weight: 500;
font-size: 1rem;
line-height: 1rem;
color: var(--accent-color);
background-color: var(--body-color);
border: 1px solid var(--divider-color);
border-radius: 1rem;
}
</style>

View File

@ -1,82 +0,0 @@
<!--
// 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 type { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
import { BuildModelKey } from '@hcengineering/view'
import {
ActionContext,
focusStore,
ListSelectionProvider,
SelectDirection,
selectionStore,
LoadingProps
} from '@hcengineering/view-resources'
import IssuesList from './IssuesList.svelte'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Employee } from '@hcengineering/contact'
import { onMount } from 'svelte'
import { IssuesGroupByKeys, IssuesOrderByKeys } from '../../utils'
export let _class: Ref<Class<Doc>>
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let itemsConfig: (BuildModelKey | string)[]
export let currentSpace: Ref<Team> | undefined = undefined
export let groupByKey: IssuesGroupByKeys | undefined = undefined
export let orderBy: IssuesOrderByKeys
export let statuses: WithLookup<IssueStatus>[]
export let employees: (WithLookup<Employee> | undefined)[] = []
export let categories: any[] = []
export let groupedIssues: { [key: string | number | symbol]: Issue[] } = {}
export let loadingProps: LoadingProps | undefined = undefined
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
let issuesList: IssuesList
$: if (issuesList !== undefined) listProvider.update(Object.values(groupedIssues).flat(1))
onMount(() => {
;(document.activeElement as HTMLElement)?.blur()
})
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<IssuesList
bind:this={issuesList}
{_class}
{baseMenuClass}
{currentSpace}
{groupByKey}
{orderBy}
{statuses}
{employees}
{categories}
{itemsConfig}
{groupedIssues}
{loadingProps}
selectedObjectIds={$selectionStore ?? []}
selectedRowIndex={listProvider.current($focusStore)}
on:row-focus={(event) => {
listProvider.updateFocus(event.detail ?? undefined)
}}
on:check={(event) => {
listProvider.updateSelection(event.detail.docs, event.detail.value)
}}
/>

View File

@ -1,21 +1,20 @@
<script lang="ts"> <script lang="ts">
import { DocumentQuery, WithLookup } from '@hcengineering/core' import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform' import { IntlString, translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker' import { Issue } from '@hcengineering/tracker'
import { Button, IconDetails, IconDetailsFilled, location } from '@hcengineering/ui' import { Button, IconDetails, IconDetailsFilled } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view' import view, { Viewlet } from '@hcengineering/view'
import { FilterBar, ViewOptionModel, ViewOptionsButton, getActiveViewletId } from '@hcengineering/view-resources' import { FilterBar, getActiveViewletId } from '@hcengineering/view-resources'
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
import tracker from '../../plugin'
import IssuesContent from './IssuesContent.svelte' import IssuesContent from './IssuesContent.svelte'
import IssuesHeader from './IssuesHeader.svelte' import IssuesHeader from './IssuesHeader.svelte'
import { getDefaultViewOptionsConfig } from '../../utils'
import tracker from '../../plugin'
import { onDestroy } from 'svelte'
export let query: DocumentQuery<Issue> = {} export let query: DocumentQuery<Issue> = {}
export let title: IntlString | undefined = undefined export let title: IntlString | undefined = undefined
export let label: string = '' export let label: string = ''
export let viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsConfig() export let space: Ref<Space> | undefined
export let panelWidth: number = 0 export let panelWidth: number = 0
@ -64,12 +63,6 @@
let docSize: boolean = false let docSize: boolean = false
$: if (docWidth <= 900 && !docSize) docSize = true $: if (docWidth <= 900 && !docSize) docSize = true
$: if (docWidth > 900 && docSize) docSize = false $: if (docWidth > 900 && docSize) docSize = false
onDestroy(
location.subscribe(() => {
viewOptionsConfig = viewOptionsConfig
})
)
</script> </script>
<IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}> <IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
@ -78,7 +71,7 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="extra"> <svelte:fragment slot="extra">
{#if viewlet} {#if viewlet}
<ViewOptionsButton viewOptionsKey={viewlet._id} config={viewOptionsConfig} /> <ViewletSettingButton {viewlet} />
{/if} {/if}
{#if asideFloat && $$slots.aside} {#if asideFloat && $$slots.aside}
<div class="buttons-divider" /> <div class="buttons-divider" />
@ -98,7 +91,7 @@
<FilterBar _class={tracker.class.Issue} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} /> <FilterBar _class={tracker.class.Issue} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
<div class="flex w-full h-full clear-mins"> <div class="flex w-full h-full clear-mins">
{#if viewlet} {#if viewlet}
<IssuesContent {viewlet} query={resultQuery} /> <IssuesContent {viewlet} query={resultQuery} {space} />
{/if} {/if}
{#if $$slots.aside !== undefined && asideShown} {#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}> <div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>

View File

@ -17,7 +17,8 @@
import { Class, Doc, DocumentQuery, Lookup, Ref, SortingOrder, WithLookup } from '@hcengineering/core' import { Class, Doc, DocumentQuery, Lookup, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { Kanban, TypeState } from '@hcengineering/kanban' import { Kanban, TypeState } from '@hcengineering/kanban'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { createQuery } from '@hcengineering/presentation' import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags' import tags from '@hcengineering/tags'
import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker' import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker'
import { import {
@ -30,18 +31,20 @@
showPopup, showPopup,
tooltip tooltip
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@hcengineering/view-resources' import { ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
import {
focusStore,
ListSelectionProvider,
noCategory,
SelectDirection,
selectionStore,
viewOptionsStore
} from '@hcengineering/view-resources'
import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte' import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte'
import Menu from '@hcengineering/view-resources/src/components/Menu.svelte' import Menu from '@hcengineering/view-resources/src/components/Menu.svelte'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import { import { getIssueStatusStates, getKanbanStatuses, getPriorityStates, issuesGroupBySorting } from '../../utils'
getIssueStatusStates,
getKanbanStatuses,
getPriorityStates,
issuesGroupBySorting,
issuesSortOrderMap
} from '../../utils'
import CreateIssue from '../CreateIssue.svelte' import CreateIssue from '../CreateIssue.svelte'
import ProjectEditor from '../projects/ProjectEditor.svelte' import ProjectEditor from '../projects/ProjectEditor.svelte'
import AssigneePresenter from './AssigneePresenter.svelte' import AssigneePresenter from './AssigneePresenter.svelte'
@ -53,24 +56,16 @@
import StatusEditor from './StatusEditor.svelte' import StatusEditor from './StatusEditor.svelte'
import EstimationEditor from './timereport/EstimationEditor.svelte' import EstimationEditor from './timereport/EstimationEditor.svelte'
export let currentSpace: Ref<Team> = tracker.team.DefaultTeam export let space: Ref<Team> | undefined = undefined
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let query: DocumentQuery<Issue> = {} export let query: DocumentQuery<Issue> = {}
export let viewOptions: { export let viewOptions: ViewOptionModel[] | undefined
groupBy: IssuesGrouping
orderBy: IssuesOrdering
shouldShowEmptyGroups: boolean
shouldShowSubIssues: boolean
}
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam $: currentSpace = space || tracker.team.DefaultTeam
$: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions) $: groupBy = ($viewOptionsStore.groupBy ?? noCategory) as IssuesGrouping
$: sort = { [orderBy]: issuesSortOrderMap[orderBy] } $: orderBy = $viewOptionsStore.orderBy
$: rankFieldName = orderBy === IssuesOrdering.Manual ? orderBy : undefined $: sort = { [orderBy[0]]: orderBy[1] }
$: resultQuery = { $: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
...(shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }),
...query
} as any
const spaceQuery = createQuery() const spaceQuery = createQuery()
const statusesQuery = createQuery() const statusesQuery = createQuery()
@ -80,6 +75,28 @@
currentTeam = res.shift() currentTeam = res.shift()
}) })
let resultQuery: DocumentQuery<any> = query
$: getResultQuery(query, viewOptions, $viewOptionsStore).then((p) => (resultQuery = p))
const client = getClient()
const hierarchy = client.getHierarchy()
async function getResultQuery (
query: DocumentQuery<Issue>,
viewOptions: ViewOptionModel[] | undefined,
viewOptionsStore: ViewOptions
): Promise<DocumentQuery<Issue>> {
if (viewOptions === undefined) return query
let result = hierarchy.clone(query)
for (const viewOption of viewOptions) {
if (viewOption.actionTartget !== 'query') continue
const queryOption = viewOption as ViewQueryOption
const f = await getResource(queryOption.action)
result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
}
return result
}
let issueStatuses: WithLookup<IssueStatus>[] | undefined let issueStatuses: WithLookup<IssueStatus>[] | undefined
$: issueStatusStates = getIssueStatusStates(issueStatuses) $: issueStatusStates = getIssueStatusStates(issueStatuses)
$: statusesQuery.query( $: statusesQuery.query(
@ -122,6 +139,12 @@
} }
const issuesQuery = createQuery() const issuesQuery = createQuery()
let issueStates: TypeState[] = [] let issueStates: TypeState[] = []
const lookupIssue: Lookup<Issue> = {
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
project: tracker.class.Project,
sprint: tracker.class.Sprint,
assignee: contact.class.Employee
}
$: issuesQuery.query( $: issuesQuery.query(
tracker.class.Issue, tracker.class.Issue,
resultQuery, resultQuery,
@ -129,12 +152,7 @@
issueStates = await getKanbanStatuses(groupBy, result) issueStates = await getKanbanStatuses(groupBy, result)
}, },
{ {
lookup: { lookup: lookupIssue,
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
project: tracker.class.Project,
sprint: tracker.class.Sprint,
assignee: contact.class.Employee
},
sort: issuesGroupBySorting[groupBy] sort: issuesGroupBySorting[groupBy]
} }
) )
@ -145,17 +163,16 @@
}) })
function getIssueStates ( function getIssueStates (
groupBy: IssuesGrouping, groupBy: IssuesGrouping,
showEmptyGroups: boolean,
states: TypeState[], states: TypeState[],
statusStates: TypeState[], statusStates: TypeState[],
priorityStates: TypeState[] priorityStates: TypeState[]
) { ) {
if (!showEmptyGroups && states.length > 0) return states if (states.length > 0) return states
if (groupBy === IssuesGrouping.Status) return statusStates if (groupBy === IssuesGrouping.Status) return statusStates
if (groupBy === IssuesGrouping.Priority) return priorityStates if (groupBy === IssuesGrouping.Priority) return priorityStates
return [] return []
} }
$: states = getIssueStates(groupBy, shouldShowEmptyGroups, issueStates, issueStatusStates, priorityStates) $: states = getIssueStates(groupBy, issueStates, issueStatusStates, priorityStates)
const fullFilled: { [key: string]: boolean } = {} const fullFilled: { [key: string]: boolean } = {}
const getState = (state: any): WithLookup<IssueStatus> | undefined => { const getState = (state: any): WithLookup<IssueStatus> | undefined => {
@ -174,12 +191,11 @@
<Kanban <Kanban
bind:this={kanbanUI} bind:this={kanbanUI}
_class={tracker.class.Issue} _class={tracker.class.Issue}
search=""
{states} {states}
{dontUpdateRank}
options={{ sort, lookup }} options={{ sort, lookup }}
query={resultQuery} query={resultQuery}
fieldName={groupBy} fieldName={groupBy}
{rankFieldName}
on:content={(evt) => { on:content={(evt) => {
listProvider.update(evt.detail) listProvider.update(evt.detail)
}} }}
@ -202,18 +218,16 @@
<span class="lines-limit-2 ml-2">{state.title}</span> <span class="lines-limit-2 ml-2">{state.title}</span>
<span class="counter ml-2 text-md">{count}</span> <span class="counter ml-2 text-md">{count}</span>
</div> </div>
{#if groupBy === IssuesGrouping.Status}
<div class="flex gap-1"> <div class="flex gap-1">
<Button <Button
icon={IconAdd} icon={IconAdd}
kind={'transparent'} kind={'transparent'}
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }} showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
on:click={() => { on:click={() => {
showPopup(CreateIssue, { space: currentSpace, status: state._id }, 'top') showPopup(CreateIssue, { space: currentSpace, [groupBy]: state._id }, 'top')
}} }}
/> />
</div> </div>
{/if}
</div> </div>
</div> </div>
</svelte:fragment> </svelte:fragment>
@ -244,7 +258,7 @@
<AssigneePresenter <AssigneePresenter
value={issue.$lookup?.assignee} value={issue.$lookup?.assignee}
defaultClass={contact.class.Employee} defaultClass={contact.class.Employee}
issueId={issue._id} object={issue}
isEditable={true} isEditable={true}
/> />
<div class="flex-center mt-2"> <div class="flex-center mt-2">
@ -252,8 +266,8 @@
</div> </div>
</div> </div>
<div class="buttons-group xsmall-gap states-bar"> <div class="buttons-group xsmall-gap states-bar">
{#if issue && issueStatuses && issue.subIssues > 0} {#if issue && issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses} /> <SubIssuesSelector value={issue} {currentTeam} />
{/if} {/if}
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} /> <PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
<ProjectEditor <ProjectEditor

View File

@ -1,83 +0,0 @@
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, DocumentQuery, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, ViewOptions } from '@hcengineering/tracker'
import { issueSP, 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 './IssuesListBrowser.svelte'
export let _class: Ref<Class<Doc>>
export let config: (string | BuildModelKey)[]
export let query: DocumentQuery<Issue> = {}
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]
$: subIssuesQuery = shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }
const statusesQuery = createQuery()
let statuses: IssueStatus[] = []
$: statusesQuery.query(
tracker.class.IssueStatus,
{ attachedTo: currentSpace },
(result) => {
statuses = [...result]
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
$: groupedIssues = groupByFunc(issues, groupBy)
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups, statuses, employees)
$: employees = issues.map((x) => x.$lookup?.assignee).filter(Boolean) as Employee[]
const issuesQuery = createQuery()
let issues: WithLookup<Issue>[] = []
$: issuesQuery.query(
tracker.class.Issue,
{ ...subIssuesQuery, ...query },
(result) => {
issues = result
},
{
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team,
sprint: tracker.class.Sprint,
_id: {
subIssues: tracker.class.Issue
}
}
}
)
</script>
<div class="w-full h-full clear-mins">
<Scroller fade={issueSP}>
<IssuesListBrowser
{_class}
{currentSpace}
{groupByKey}
orderBy={orderByKey}
{statuses}
{employees}
{categories}
itemsConfig={config}
{groupedIssues}
/>
</Scroller>
</div>

View File

@ -0,0 +1,74 @@
<!--
// Copyright © 2023 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 { IssuePriority } from '@hcengineering/tracker'
import { Button, ButtonKind, ButtonSize, Icon, Label } from '@hcengineering/ui'
import { issuePriorities } from '../../utils'
export let value: IssuePriority
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
</script>
{#if kind === 'list' || kind === 'list-header'}
<div class="priority-container">
<div class="icon">
{#if issuePriorities[value]?.icon}<Icon icon={issuePriorities[value]?.icon} {size} />{/if}
</div>
<span
class="{kind === 'list' ? 'ml-2 text-md' : 'ml-3 text-base'} overflow-label disabled fs-bold content-accent-color"
>
<Label label={issuePriorities[value]?.label} />
</span>
</div>
{:else}
<Button
label={issuePriorities[value]?.label}
icon={issuePriorities[value]?.icon}
{justify}
{width}
{size}
{kind}
disabled
/>
{/if}
<style lang="scss">
.priority-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);
}
&:hover {
.icon {
color: var(--caption-color) !important;
}
}
}
</style>

View File

@ -0,0 +1,32 @@
<!--
// Copyright © 2023 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, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import tracker, { IssueStatus } from '@hcengineering/tracker'
import StatusPresenter from './StatusPresenter.svelte'
export let value: Ref<IssueStatus> | undefined
export let size: 'small' | 'medium' = 'medium'
let status: WithLookup<IssueStatus> | undefined
const query = createQuery()
$: query.query(tracker.class.IssueStatus, { _id: value }, (res) => ([status] = res), {
lookup: { category: tracker.class.IssueStatusCategory }
})
</script>
<StatusPresenter value={status} {size} />

View File

@ -20,7 +20,7 @@
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation' import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting' import setting, { settingId } from '@hcengineering/setting'
import type { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker' import type { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { import {
Button, Button,
EditBox, EditBox,
@ -33,14 +33,7 @@
showPopup, showPopup,
Spinner Spinner
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { import { ContextMenu, UpDownNavigator } from '@hcengineering/view-resources'
ContextMenu,
focusStore,
ListSelectionProvider,
SelectDirection,
UpDownNavigator,
viewOptionsStore
} from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte' import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues' import { generateIssueShortLink, getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
@ -49,8 +42,6 @@
import CopyToClipboard from './CopyToClipboard.svelte' import CopyToClipboard from './CopyToClipboard.svelte'
import SubIssues from './SubIssues.svelte' import SubIssues from './SubIssues.svelte'
import SubIssueSelector from './SubIssueSelector.svelte' import SubIssueSelector from './SubIssueSelector.svelte'
import { groupBy as groupByFunc, issuesOrderKeyMap, issuesSortOrderMap } from '../../../utils'
import contact from '@hcengineering/contact'
export let _id: Ref<Issue> export let _id: Ref<Issue>
export let _class: Ref<Class<Issue>> export let _class: Ref<Class<Issue>>
@ -71,14 +62,6 @@
let isEditing = false let isEditing = false
let descriptionBox: AttachmentStyledBox let descriptionBox: AttachmentStyledBox
let groupBy: IssuesGrouping
let orderBy: IssuesOrdering
let shouldShowSubIssues: boolean
$: ({ groupBy, orderBy, shouldShowSubIssues } = $viewOptionsStore)
$: orderByKey = issuesOrderKeyMap[orderBy]
$: subIssuesQuery = shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }
$: query = { space: issue?.space }
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res()) const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
$: read(_id) $: read(_id)
@ -126,72 +109,6 @@
$: isDescriptionEmpty = !new DOMParser().parseFromString(description, 'text/html').documentElement.innerText?.trim() $: isDescriptionEmpty = !new DOMParser().parseFromString(description, 'text/html').documentElement.innerText?.trim()
$: parentIssue = issue?.$lookup?.attachedTo $: parentIssue = issue?.$lookup?.attachedTo
let issues: WithLookup<Issue>[] = []
let neighbourIssues: Issue[] = []
const issuesQuery = createQuery()
const subIssuesQueryClient = createQuery()
$: if (parentIssue) {
subIssuesQueryClient.query(
tracker.class.Issue,
{ attachedTo: parentIssue?._id },
async (result) => (neighbourIssues = result ?? []),
{
sort: { rank: SortingOrder.Descending }
}
)
} else {
issuesQuery.query(
tracker.class.Issue,
{ ...subIssuesQuery, ...query },
(result) => {
issues = result
},
{
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team,
sprint: tracker.class.Sprint,
_id: {
subIssues: tracker.class.Issue
}
}
}
)
}
$: groupedIssues = groupByFunc(issues, groupBy)
$: flatGroupedIssues = Object.values(groupedIssues ?? {}).flat(1)
$: issuesToNavigate = parentIssue ? neighbourIssues : flatGroupedIssues
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
if (groupedIssues) {
const selectedRowIndex = listProvider.current($focusStore)
let position =
(of !== undefined ? issuesToNavigate.findIndex((x) => x._id === of?._id) : selectedRowIndex) ?? -1
position -= offset
if (position < 0) {
position = 0
}
if (position >= issuesToNavigate.length) {
position = issuesToNavigate.length - 1
}
listProvider.updateFocus(issuesToNavigate[position])
}
}
})
$: if (issue) listProvider.updateFocus(issue)
$: listProvider.update(issuesToNavigate)
function edit (ev: MouseEvent) { function edit (ev: MouseEvent) {
ev.preventDefault() ev.preventDefault()

View File

@ -21,6 +21,7 @@
import { flip } from 'svelte/animate' import { flip } from 'svelte/animate'
import { getIssueId } from '../../../issues' import { getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { subIssueListProvider } from '../../../utils'
import Circles from '../../icons/Circles.svelte' import Circles from '../../icons/Circles.svelte'
import AssigneeEditor from '../AssigneeEditor.svelte' import AssigneeEditor from '../AssigneeEditor.svelte'
import DueDateEditor from '../DueDateEditor.svelte' import DueDateEditor from '../DueDateEditor.svelte'
@ -41,6 +42,7 @@
function openIssue (target: Issue) { function openIssue (target: Issue) {
dispatch('issue-focus', target) dispatch('issue-focus', target)
subIssueListProvider(issues, target._id)
showPanel(tracker.component.EditIssue, target._id, target._class, 'content') showPanel(tracker.component.EditIssue, target._id, target._class, 'content')
} }
@ -120,7 +122,7 @@
{issue.title} {issue.title}
</span> </span>
{#if issue.subIssues > 0} {#if issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses.get(issue.space)} /> <SubIssuesSelector value={issue} {currentTeam} />
{/if} {/if}
</div> </div>
<div class="flex-center flex-no-shrink"> <div class="flex-center flex-no-shrink">

View File

@ -31,6 +31,7 @@
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { getIssueId } from '../../../issues' import { getIssueId } from '../../../issues'
import IssueStatusIcon from '../IssueStatusIcon.svelte' import IssueStatusIcon from '../IssueStatusIcon.svelte'
import { ListSelectionProvider } from '@hcengineering/view-resources'
export let issue: WithLookup<Issue> export let issue: WithLookup<Issue>
@ -48,6 +49,7 @@
function openParentIssue () { function openParentIssue () {
if (parentIssue) { if (parentIssue) {
closeTooltip() closeTooltip()
ListSelectionProvider.Pop()
openIssue(parentIssue._id) openIssue(parentIssue._id)
} }
} }
@ -138,7 +140,7 @@
bind:this={subIssuesElement} bind:this={subIssuesElement}
class="flex-center sub-issues cursor-pointer" class="flex-center sub-issues cursor-pointer"
use:tooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }} use:tooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }}
on:click|preventDefault={areSubIssuesLoading ? undefined : showSubIssues} on:click|preventDefault={showSubIssues}
> >
<span class="overflow-label">{subIssues?.length}</span> <span class="overflow-label">{subIssues?.length}</span>
<div class="ml-2"> <div class="ml-2">

View File

@ -13,17 +13,26 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Doc, Ref, WithLookup } from '@hcengineering/core' import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker' import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { ButtonKind, ButtonSize, getPlatformColor } from '@hcengineering/ui' import {
import { Button, closeTooltip, ProgressCircle, SelectPopup, showPanel, showPopup } from '@hcengineering/ui' Button,
import { updateFocus } from '@hcengineering/view-resources' ButtonKind,
import tracker from '../../../plugin' ButtonSize,
closeTooltip,
getPlatformColor,
ProgressCircle,
SelectPopup,
showPanel,
showPopup
} from '@hcengineering/ui'
import { getIssueId } from '../../../issues' import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import { subIssueListProvider } from '../../../utils'
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
export let currentTeam: Team | undefined export let currentTeam: Team | undefined
export let statuses: WithLookup<IssueStatus>[] | undefined
export let kind: ButtonKind = 'link-bordered' export let kind: ButtonKind = 'link-bordered'
export let size: ButtonSize = 'inline' export let size: ButtonSize = 'inline'
@ -32,21 +41,35 @@
let btn: HTMLElement let btn: HTMLElement
let subIssues: Issue[] | undefined let subIssues: Issue[] = []
let doneStatus: Ref<Doc> | undefined
let countComplate: number = 0 let countComplate: number = 0
const query = createQuery()
const statusesQuery = createQuery()
let statuses: WithLookup<IssueStatus>[] = []
$: if (value.$lookup?.subIssues !== undefined) { $: if (value.$lookup?.subIssues !== undefined) {
query.unsubscribe()
subIssues = value.$lookup.subIssues as Issue[] subIssues = value.$lookup.subIssues as Issue[]
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? '')) subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
} else {
query.query(tracker.class.Issue, { attachedTo: value._id }, (res) => (subIssues = res), {
sort: { rank: SortingOrder.Ascending }
})
} }
statusesQuery.query(tracker.class.IssueStatus, {}, (res) => (statuses = res), {
lookup: { category: tracker.class.IssueStatusCategory }
})
$: if (statuses && subIssues) { $: if (statuses && subIssues) {
doneStatus = statuses.find((s) => s.category === tracker.issueStatusCategory.Completed)?._id ?? undefined const doneStatuses = statuses.filter((s) => s.category === tracker.issueStatusCategory.Completed).map((p) => p._id)
if (doneStatus) countComplate = subIssues.filter((si) => si.status === doneStatus).length countComplate = subIssues.filter((si) => doneStatuses.includes(si.status)).length
} }
$: hasSubIssues = (subIssues?.length ?? 0) > 0 $: hasSubIssues = (subIssues?.length ?? 0) > 0
function getIssueStatusIcon (issue: Issue) { function getIssueStatusIcon (issue: Issue, statuses: WithLookup<IssueStatus>[] | undefined) {
const status = statuses?.find((s) => issue.status === s._id) const status = statuses?.find((s) => issue.status === s._id)
const category = status?.$lookup?.category const category = status?.$lookup?.category
const color = status?.color ?? category?.color const color = status?.color ?? category?.color
@ -59,9 +82,11 @@
function openIssue (target: Ref<Issue>) { function openIssue (target: Ref<Issue>) {
if (target !== value._id) { if (target !== value._id) {
subIssueListProvider(subIssues, target)
showPanel(tracker.component.EditIssue, target, value._class, 'content') showPanel(tracker.component.EditIssue, target, value._class, 'content')
} }
} }
function showSubIssues () { function showSubIssues () {
if (subIssues) { if (subIssues) {
closeTooltip() closeTooltip()
@ -71,7 +96,7 @@
value: subIssues.map((iss) => { value: subIssues.map((iss) => {
const text = currentTeam ? `${getIssueId(currentTeam, iss)} ${iss.title}` : iss.title const text = currentTeam ? `${getIssueId(currentTeam, iss)} ${iss.title}` : iss.title
return { id: iss._id, text, isSelected: iss._id === value._id, ...getIssueStatusIcon(iss) } return { id: iss._id, text, isSelected: iss._id === value._id, ...getIssueStatusIcon(iss, statuses) }
}), }),
width: 'large' width: 'large'
}, },
@ -86,12 +111,6 @@
}, },
(selectedIssue) => { (selectedIssue) => {
selectedIssue !== undefined && openIssue(selectedIssue) selectedIssue !== undefined && openIssue(selectedIssue)
},
(selectedIssue) => {
const focus = subIssues?.find((it) => it._id === selectedIssue.id)
if (focus !== undefined) {
updateFocus({ focus })
}
} }
) )
} }

View File

@ -70,7 +70,7 @@
config={[ config={[
'$lookup.attachedTo', '$lookup.attachedTo',
'', '',
'$lookup.employee', 'employee',
{ {
key: '$lookup.attachedTo', key: '$lookup.attachedTo',
presenter: ParentNamesPresenter, presenter: ParentNamesPresenter,

View File

@ -19,10 +19,8 @@
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import type { Issue } from '@hcengineering/tracker' import type { Issue } from '@hcengineering/tracker'
import { ViewOptionModel } from '@hcengineering/view-resources'
import tracker from '../../plugin' import tracker from '../../plugin'
import { getDefaultViewOptionsConfig } from '../../utils'
import IssuesView from '../issues/IssuesView.svelte' import IssuesView from '../issues/IssuesView.svelte'
import ModeSelector from '../ModeSelector.svelte' import ModeSelector from '../ModeSelector.svelte'
@ -36,8 +34,6 @@
let created = { _id: { $in: [] as Ref<Issue>[] } } let created = { _id: { $in: [] as Ref<Issue>[] } }
let subscribed = { _id: { $in: [] as Ref<Issue>[] } } let subscribed = { _id: { $in: [] as Ref<Issue>[] } }
const viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsConfig(true)
const createdQuery = createQuery() const createdQuery = createQuery()
$: createdQuery.query<TxCollectionCUD<Issue, Issue>>( $: createdQuery.query<TxCollectionCUD<Issue, Issue>>(
core.class.TxCollectionCUD, core.class.TxCollectionCUD,
@ -79,7 +75,7 @@
$: query = getQuery(mode, { assigned, created, subscribed }) $: query = getQuery(mode, { assigned, created, subscribed })
</script> </script>
<IssuesView {query} title={tracker.string.MyIssues} {viewOptionsConfig}> <IssuesView {query} space={undefined} title={tracker.string.MyIssues}>
<svelte:fragment slot="afterHeader"> <svelte:fragment slot="afterHeader">
<ModeSelector {config} {mode} onChange={handleChangeMode} /> <ModeSelector {config} {mode} onChange={handleChangeMode} />
</svelte:fragment> </svelte:fragment>

View File

@ -34,7 +34,7 @@
}) })
</script> </script>
<IssuesView query={{ project: project._id, space: project.space }} label={project.label}> <IssuesView query={{ project: project._id, space: project.space }} space={project.space} label={project.label}>
<svelte:fragment slot="label_selector"> <svelte:fragment slot="label_selector">
<Button size={'small'} kind={'link'} on:click={selectProject}> <Button size={'small'} kind={'link'} on:click={selectProject}>
<svelte:fragment slot="content"> <svelte:fragment slot="content">

View File

@ -44,7 +44,7 @@
}) })
</script> </script>
<IssuesView query={{ sprint: sprint._id, space: sprint.space }} label={sprint.label}> <IssuesView query={{ sprint: sprint._id, space: sprint.space }} space={sprint.space} label={sprint.label}>
<svelte:fragment slot="label_selector"> <svelte:fragment slot="label_selector">
<div bind:this={container}> <div bind:this={container}>
<Button size={'small'} kind={'link'} on:click={selectSprint}> <Button size={'small'} kind={'link'} on:click={selectSprint}>

View File

@ -20,13 +20,13 @@
import tracker from '../../plugin' import tracker from '../../plugin'
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte' import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
import TimePresenter from '../issues/timereport/TimePresenter.svelte' import TimePresenter from '../issues/timereport/TimePresenter.svelte'
export let issues: Issue[] | undefined = undefined export let docs: Issue[] | undefined = undefined
export let capacity: number | undefined = undefined export let capacity: number | undefined = undefined
export let workDayLength: WorkDayLength = WorkDayLength.EIGHT_HOURS export let workDayLength: WorkDayLength = WorkDayLength.EIGHT_HOURS
$: ids = new Set(issues?.map((it) => it._id) ?? []) $: ids = new Set(docs?.map((it) => it._id) ?? [])
$: noParents = issues?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>)) $: noParents = docs?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
$: rootNoBacklogIssues = noParents?.filter( $: rootNoBacklogIssues = noParents?.filter(
(it) => issueStatuses.get(it.status)?.category !== tracker.issueStatusCategory.Backlog (it) => issueStatuses.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
@ -86,12 +86,12 @@
}) })
.reduce((it, cur) => { .reduce((it, cur) => {
return it + cur return it + cur
}), }, 0),
3 3
) )
</script> </script>
{#if issues} {#if docs}
<!-- <Label label={tracker.string.SprintDay} value={}/> --> <!-- <Label label={tracker.string.SprintDay} value={}/> -->
<div class="flex-row-center flex-no-shrink h-6" class:showWarning={totalEstimation > (capacity ?? 0)}> <div class="flex-row-center flex-no-shrink h-6" class:showWarning={totalEstimation > (capacity ?? 0)}>
<EstimationProgressCircle value={totalReported} max={totalEstimation} /> <EstimationProgressCircle value={totalReported} max={totalEstimation} />

View File

@ -17,8 +17,13 @@
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueTemplate, Sprint, Team } from '@hcengineering/tracker' import { Issue, IssueTemplate, Sprint, Team } from '@hcengineering/tracker'
import { ButtonKind, ButtonShape, ButtonSize, deviceOptionsStore as deviceInfo } from '@hcengineering/ui' import {
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte' ButtonKind,
ButtonShape,
ButtonSize,
DatePresenter,
deviceOptionsStore as deviceInfo
} from '@hcengineering/ui'
import { activeSprint } from '../../issues' import { activeSprint } from '../../issues'
import tracker from '../../plugin' import tracker from '../../plugin'
import { getDayOfSprint } from '../../utils' import { getDayOfSprint } from '../../utils'

View File

@ -0,0 +1,76 @@
<!--
// Copyright © 2023 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 { createQuery } from '@hcengineering/presentation'
import { Sprint, Team } from '@hcengineering/tracker'
import { ButtonKind, DatePresenter, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import tracker from '../../plugin'
import { getDayOfSprint } from '../../utils'
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
import SprintSelector from './SprintSelector.svelte'
export let value: Ref<Sprint>
export let kind: ButtonKind = 'link'
const spaceQuery = createQuery()
let currentTeam: Team | undefined
$: sprint &&
spaceQuery.query(tracker.class.Team, { _id: sprint.space }, (res) => {
;[currentTeam] = res
})
$: workDayLength = currentTeam?.workDayLength
const sprintQuery = createQuery()
let sprint: Sprint | undefined
$: sprintQuery.query(tracker.class.Sprint, { _id: value }, (res) => {
;[sprint] = res
})
$: twoRows = $deviceInfo.twoRows
</script>
<div
class="flex flex-wrap"
class:minus-margin={kind === 'list-header'}
style:flex-direction={twoRows ? 'column' : 'row'}
>
<div class="flex-row-center" class:minus-margin-vSpace={kind === 'list-header'}>
<SprintSelector {kind} isEditable={false} enlargedText {value} />
</div>
{#if sprint && kind === 'list-header'}
<div class="flex-row-center" class:minus-margin-space={kind === 'list-header'} class:text-sm={twoRows}>
{#if sprint}
{@const now = Date.now()}
{@const sprintDaysFrom =
now < sprint.startDate
? 0
: now > sprint.targetDate
? getDayOfSprint(sprint.startDate, sprint.targetDate)
: getDayOfSprint(sprint.startDate, now)}
{@const sprintDaysTo = getDayOfSprint(sprint.startDate, sprint.targetDate)}
<DatePresenter value={sprint.startDate} kind={'transparent'} />
<span class="p-1"> / </span>
<DatePresenter value={sprint.targetDate} kind={'transparent'} />
<div class="w-2 min-w-2" />
<!-- Active sprint in time -->
<TimePresenter value={sprintDaysFrom} {workDayLength} />
/
<TimePresenter value={sprintDaysTo} {workDayLength} />
{/if}
</div>
{/if}
</div>

View File

@ -1,17 +1,30 @@
<script lang="ts"> <script lang="ts">
import { DocumentQuery, WithLookup } from '@hcengineering/core' import { DocumentQuery, WithLookup } from '@hcengineering/core'
import { IssueTemplate, ViewOptions } from '@hcengineering/tracker' import { IssueTemplate } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
import { Viewlet } from '@hcengineering/view' import { Viewlet } from '@hcengineering/view'
import { viewOptionsStore } from '@hcengineering/view-resources'
import TemplatesList from './TemplatesList.svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import CreateIssueTemplate from './CreateIssueTemplate.svelte'
export let viewlet: WithLookup<Viewlet> export let viewlet: WithLookup<Viewlet>
export let query: DocumentQuery<IssueTemplate> = {} export let query: DocumentQuery<IssueTemplate> = {}
$: vo = $viewOptionsStore as ViewOptions const createItemDialog = CreateIssueTemplate
const createItemLabel = tracker.string.IssueTemplate
</script> </script>
{#if viewlet?.$lookup?.descriptor?.component} {#if viewlet?.$lookup?.descriptor?.component}
<TemplatesList _class={tracker.class.IssueTemplate} config={viewlet.config} {query} viewOptions={vo} /> <Component
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.IssueTemplate,
config: viewlet.config,
options: viewlet.options,
createItemDialog,
createItemLabel,
viewOptions: viewlet.viewOptions?.other,
viewlet,
query
}}
/>
{/if} {/if}

View File

@ -4,8 +4,9 @@
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { IssueTemplate } from '@hcengineering/tracker' import { IssueTemplate } from '@hcengineering/tracker'
import { Button, IconAdd, IconDetails, IconDetailsFilled, showPopup } from '@hcengineering/ui' import { Button, IconAdd, IconDetails, IconDetailsFilled, showPopup } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view' import view, { Viewlet, ViewOptionModel } from '@hcengineering/view'
import { FilterBar, getActiveViewletId, ViewOptionModel, ViewOptionsButton } from '@hcengineering/view-resources' import { FilterBar, getActiveViewletId } from '@hcengineering/view-resources'
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import { getDefaultViewOptionsTemplatesConfig } from '../../utils' import { getDefaultViewOptionsTemplatesConfig } from '../../utils'
import IssuesHeader from '../issues/IssuesHeader.svelte' import IssuesHeader from '../issues/IssuesHeader.svelte'
@ -85,7 +86,7 @@
/> />
{#if viewlet} {#if viewlet}
<ViewOptionsButton viewOptionsKey={viewlet._id} config={viewOptionsConfig} /> <ViewletSettingButton {viewlet} />
{/if} {/if}
{#if asideFloat && $$slots.aside} {#if asideFloat && $$slots.aside}

View File

@ -1,65 +0,0 @@
<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>

View File

@ -32,8 +32,8 @@ import IssuePreview from './components/issues/IssuePreview.svelte'
import Issues from './components/issues/Issues.svelte' import Issues from './components/issues/Issues.svelte'
import IssuesView from './components/issues/IssuesView.svelte' import IssuesView from './components/issues/IssuesView.svelte'
import KanbanView from './components/issues/KanbanView.svelte' import KanbanView from './components/issues/KanbanView.svelte'
import ListView from './components/issues/ListView.svelte'
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte' import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
import PriorityRefPresenter from './components/issues/PriorityRefPresenter.svelte'
import PriorityEditor from './components/issues/PriorityEditor.svelte' import PriorityEditor from './components/issues/PriorityEditor.svelte'
import PriorityPresenter from './components/issues/PriorityPresenter.svelte' import PriorityPresenter from './components/issues/PriorityPresenter.svelte'
import StatusEditor from './components/issues/StatusEditor.svelte' import StatusEditor from './components/issues/StatusEditor.svelte'
@ -79,7 +79,6 @@ import SprintStatusPresenter from './components/sprints/SprintStatusPresenter.sv
import SprintTitlePresenter from './components/sprints/SprintTitlePresenter.svelte' import SprintTitlePresenter from './components/sprints/SprintTitlePresenter.svelte'
import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte' import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte'
import GrowPresenter from './components/issues/GrowPresenter.svelte'
import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte' import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte'
import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.svelte' import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.svelte'
import TimeSpendReport from './components/issues/timereport/TimeSpendReport.svelte' import TimeSpendReport from './components/issues/timereport/TimeSpendReport.svelte'
@ -95,11 +94,14 @@ import IssueTemplates from './components/templates/IssueTemplates.svelte'
import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte' import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte' import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
import MoveAndDeleteSprintPopup from './components/sprints/MoveAndDeleteSprintPopup.svelte' import MoveAndDeleteSprintPopup from './components/sprints/MoveAndDeleteSprintPopup.svelte'
import { moveIssuesToAnotherSprint } from './utils' import { moveIssuesToAnotherSprint, issueStatusSort, issuePrioritySort, sprintSort, subIssueQuery } from './utils'
import { deleteObject } from '@hcengineering/view-resources/src/utils' import { deleteObject } from '@hcengineering/view-resources/src/utils'
import CreateTeam from './components/teams/CreateTeam.svelte' import CreateTeam from './components/teams/CreateTeam.svelte'
import TeamPresenter from './components/teams/TeamPresenter.svelte' import TeamPresenter from './components/teams/TeamPresenter.svelte'
import IssueStatistics from './components/sprints/IssueStatistics.svelte'
import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte'
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
export async function queryIssue<D extends Issue> ( export async function queryIssue<D extends Issue> (
_class: Ref<Class<D>>, _class: Ref<Class<D>>,
@ -229,6 +231,8 @@ export default async (): Promise<Resources> => ({
ModificationDatePresenter, ModificationDatePresenter,
PriorityPresenter, PriorityPresenter,
PriorityEditor, PriorityEditor,
PriorityRefPresenter,
SprintRefPresenter,
ProjectEditor, ProjectEditor,
StatusPresenter, StatusPresenter,
StatusEditor, StatusEditor,
@ -246,7 +250,6 @@ export default async (): Promise<Resources> => ({
SetParentIssueActionPopup, SetParentIssueActionPopup,
EditProject, EditProject,
IssuesView, IssuesView,
ListView,
KanbanView, KanbanView,
TeamProjects, TeamProjects,
Roadmap, Roadmap,
@ -265,7 +268,6 @@ export default async (): Promise<Resources> => ({
TimeSpendReport, TimeSpendReport,
EstimationEditor, EstimationEditor,
SubIssuesSelector, SubIssuesSelector,
GrowPresenter,
RelatedIssues, RelatedIssues,
RelatedIssueTemplates, RelatedIssueTemplates,
ProjectSelector, ProjectSelector,
@ -274,7 +276,9 @@ export default async (): Promise<Resources> => ({
EditIssueTemplate, EditIssueTemplate,
TemplateEstimationEditor, TemplateEstimationEditor,
CreateTeam, CreateTeam,
TeamPresenter TeamPresenter,
IssueStatistics,
StatusRefPresenter
}, },
completion: { completion: {
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) => IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
@ -284,7 +288,11 @@ export default async (): Promise<Resources> => ({
IssueTitleProvider: getIssueTitle, IssueTitleProvider: getIssueTitle,
GetIssueId: issueIdProvider, GetIssueId: issueIdProvider,
GetIssueLink: issueLinkProvider, GetIssueLink: issueLinkProvider,
GetIssueTitle: issueTitleProvider GetIssueTitle: issueTitleProvider,
IssueStatusSort: issueStatusSort,
IssuePrioritySort: issuePrioritySort,
SprintSort: sprintSort,
SubIssueQuery: subIssueQuery
}, },
actionImpl: { actionImpl: {
EditWorkflowStatuses: editWorkflowStatuses, EditWorkflowStatuses: editWorkflowStatuses,

View File

@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { Client, Doc, Ref } from '@hcengineering/core' import { Client, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString, Metadata, Resource } from '@hcengineering/platform' import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import tracker, { trackerId } from '../../tracker/lib' import tracker, { trackerId } from '../../tracker/lib'
import { IssueDraft } from '@hcengineering/tracker' import { IssueDraft } from '@hcengineering/tracker'
import { SortFunc } from '@hcengineering/view'
export default mergeIds(trackerId, tracker, { export default mergeIds(trackerId, tracker, {
string: { string: {
@ -292,9 +293,11 @@ export default mergeIds(trackerId, tracker, {
ModificationDatePresenter: '' as AnyComponent, ModificationDatePresenter: '' as AnyComponent,
PriorityPresenter: '' as AnyComponent, PriorityPresenter: '' as AnyComponent,
PriorityEditor: '' as AnyComponent, PriorityEditor: '' as AnyComponent,
PriorityRefPresenter: '' as AnyComponent,
ProjectEditor: '' as AnyComponent, ProjectEditor: '' as AnyComponent,
SprintEditor: '' as AnyComponent, SprintEditor: '' as AnyComponent,
StatusPresenter: '' as AnyComponent, StatusPresenter: '' as AnyComponent,
StatusRefPresenter: '' as AnyComponent,
StatusEditor: '' as AnyComponent, StatusEditor: '' as AnyComponent,
AssigneePresenter: '' as AnyComponent, AssigneePresenter: '' as AnyComponent,
DueDatePresenter: '' as AnyComponent, DueDatePresenter: '' as AnyComponent,
@ -312,13 +315,12 @@ export default mergeIds(trackerId, tracker, {
SetParentIssueActionPopup: '' as AnyComponent, SetParentIssueActionPopup: '' as AnyComponent,
EditProject: '' as AnyComponent, EditProject: '' as AnyComponent,
IssuesView: '' as AnyComponent, IssuesView: '' as AnyComponent,
ListView: '' as AnyComponent,
KanbanView: '' as AnyComponent, KanbanView: '' as AnyComponent,
Roadmap: '' as AnyComponent, Roadmap: '' as AnyComponent,
TeamProjects: '' as AnyComponent, TeamProjects: '' as AnyComponent,
IssuePreview: '' as AnyComponent, IssuePreview: '' as AnyComponent,
RelationsPopup: '' as AnyComponent, RelationsPopup: '' as AnyComponent,
SprintRefPresenter: '' as AnyComponent,
Sprints: '' as AnyComponent, Sprints: '' as AnyComponent,
SprintPresenter: '' as AnyComponent, SprintPresenter: '' as AnyComponent,
SprintStatusPresenter: '' as AnyComponent, SprintStatusPresenter: '' as AnyComponent,
@ -328,7 +330,6 @@ export default mergeIds(trackerId, tracker, {
TimeSpendReport: '' as AnyComponent, TimeSpendReport: '' as AnyComponent,
EstimationEditor: '' as AnyComponent, EstimationEditor: '' as AnyComponent,
TemplateEstimationEditor: '' as AnyComponent, TemplateEstimationEditor: '' as AnyComponent,
GrowPresenter: '' as AnyComponent,
ProjectSelector: '' as AnyComponent, ProjectSelector: '' as AnyComponent,
@ -342,6 +343,10 @@ export default mergeIds(trackerId, tracker, {
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>, IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
GetIssueId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>, GetIssueId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>, GetIssueLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueTitle: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>> GetIssueTitle: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
IssueStatusSort: '' as SortFunc,
IssuePrioritySort: '' as SortFunc,
SprintSort: '' as SortFunc,
SubIssueQuery: '' as Resource<(value: any, query: DocumentQuery<Doc>) => DocumentQuery<Doc>>
} }
}) })

View File

@ -102,5 +102,5 @@ export const issuesGroupBySorting: Record<IssuesGrouping, SortingQuery<Issue>> =
[IssuesGrouping.Priority]: { priority: SortingOrder.Ascending }, [IssuesGrouping.Priority]: { priority: SortingOrder.Ascending },
[IssuesGrouping.Project]: { '$lookup.project.label': SortingOrder.Ascending }, [IssuesGrouping.Project]: { '$lookup.project.label': SortingOrder.Ascending },
[IssuesGrouping.Sprint]: { '$lookup.sprint.label': SortingOrder.Ascending }, [IssuesGrouping.Sprint]: { '$lookup.sprint.label': SortingOrder.Ascending },
[IssuesGrouping.NoGrouping]: {} [IssuesGrouping.NoGrouping]: { rank: SortingOrder.Ascending }
} }

View File

@ -13,24 +13,23 @@
// limitations under the License. // limitations under the License.
// //
import { Employee, formatName } from '@hcengineering/contact' import { Doc, DocumentQuery, Ref, SortingOrder, TxOperations, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, SortingOrder, TxOperations, WithLookup } from '@hcengineering/core'
import { TypeState } from '@hcengineering/kanban' import { TypeState } from '@hcengineering/kanban'
import { Asset, IntlString, translate } from '@hcengineering/platform' import { Asset, IntlString, translate } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { import {
Issue, Issue,
IssuePriority,
IssuesDateModificationPeriod, IssuesDateModificationPeriod,
IssuesGrouping, IssuesGrouping,
IssuesOrdering, IssuesOrdering,
IssueStatus, IssueStatus,
IssueTemplate,
ProjectStatus, ProjectStatus,
Sprint, Sprint,
SprintStatus, SprintStatus,
Team, Team,
TimeReportDayType TimeReportDayType
} from '@hcengineering/tracker' } from '@hcengineering/tracker'
import { ViewOptionModel } from '@hcengineering/view-resources'
import { import {
AnyComponent, AnyComponent,
AnySvelteComponent, AnySvelteComponent,
@ -39,6 +38,8 @@ import {
isWeekend, isWeekend,
MILLISECONDS_IN_WEEK MILLISECONDS_IN_WEEK
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { ViewOptionModel } from '@hcengineering/view'
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import tracker from './plugin' import tracker from './plugin'
import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types' import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types'
@ -306,94 +307,38 @@ const listIssueStatusOrder = [
tracker.issueStatusCategory.Canceled tracker.issueStatusCategory.Canceled
] as const ] as const
export function getCategories ( export async function issueStatusSort (value: Array<Ref<IssueStatus>>): Promise<Array<Ref<IssueStatus>>> {
key: IssuesGroupByKeys | undefined, return await new Promise((resolve) => {
elements: Array<WithLookup<Issue | IssueTemplate>>, const query = createQuery(true)
shouldShowAll: boolean, query.query(tracker.class.IssueStatus, { _id: { $in: value } }, (res) => {
statuses: IssueStatus[], res.sort((a, b) => listIssueStatusOrder.indexOf(a.category) - listIssueStatusOrder.indexOf(b.category))
employees: Employee[] resolve(res.map((p) => p._id))
): any[] { query.unsubscribe()
if (key === undefined) { })
return [undefined] // No grouping })
}
const defaultStatuses = listIssueStatusOrder
.map((category) => statuses.filter((status) => status.category === category).map((item) => item._id))
.flat()
const existingCategories = Array.from(new Set(elements.map((x: any) => x[key] ?? undefined)))
if (shouldShowAll) {
if (key === 'status') {
return defaultStatuses
} }
if (key === 'priority') { export async function issuePrioritySort (value: IssuePriority[]): Promise<IssuePriority[]> {
return defaultPriorities value.sort((a, b) => {
} const i1 = defaultPriorities.indexOf(a)
} const i2 = defaultPriorities.indexOf(b)
if (key === 'status') {
existingCategories.sort((s1, s2) => {
const i1 = defaultStatuses.findIndex((x) => x === s1)
const i2 = defaultStatuses.findIndex((x) => x === s2)
return i1 - i2 return i1 - i2
}) })
return value
} }
if (key === 'priority') { export async function sprintSort (value: Array<Ref<Sprint>>): Promise<Array<Ref<Sprint>>> {
existingCategories.sort((p1, p2) => { return await new Promise((resolve) => {
const i1 = defaultPriorities.findIndex((x) => x === p1) const query = createQuery(true)
const i2 = defaultPriorities.findIndex((x) => x === p2) query.query(tracker.class.Sprint, { _id: { $in: value } }, (res) => {
res.sort((a, b) => (b?.startDate ?? 0) - (a?.startDate ?? 0))
return i1 - i2 resolve(res.map((p) => p._id))
query.unsubscribe()
}) })
}
if (key === 'sprint') {
const sprints = new Map(elements.map((x) => [x.sprint, x.$lookup?.sprint]))
existingCategories.sort((p1, p2) => {
const i1 = sprints.get(p1 as Ref<Sprint>)
const i2 = sprints.get(p2 as Ref<Sprint>)
return (i2?.startDate ?? 0) - (i1?.startDate ?? 0)
}) })
} }
if (key === 'assignee') {
existingCategories.sort((a1, a2) => {
const employeeId1 = a1 as Ref<Employee> | null
const employeeId2 = a2 as Ref<Employee> | null
if (employeeId1 === null && employeeId2 !== null) {
return 1
}
if (employeeId1 !== null && employeeId2 === null) {
return -1
}
if (employeeId1 !== null && employeeId2 !== null) {
const name1 = formatName(employees.find((x) => x?._id === employeeId1)?.name ?? '')
const name2 = formatName(employees.find((x) => x?._id === employeeId2)?.name ?? '')
if (name1 > name2) {
return 1
} else if (name2 > name1) {
return -1
}
return 0
}
return 0
})
}
return existingCategories
}
export async function getKanbanStatuses ( export async function getKanbanStatuses (
groupBy: IssuesGrouping, groupBy: IssuesGrouping,
issues: Array<WithLookup<Issue>> issues: Array<WithLookup<Issue>>
@ -506,55 +451,6 @@ export async function getPriorityStates (): Promise<TypeState[]> {
) )
} }
export function getDefaultViewOptionsConfig (subIssuesValue = false): 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: subIssuesValue,
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]
result.push(showSubIssuesCategory)
result.push(showEmptyGroups)
return result
}
export function getDefaultViewOptionsTemplatesConfig (): ViewOptionModel[] { export function getDefaultViewOptionsTemplatesConfig (): ViewOptionModel[] {
const groupByCategory: ViewOptionModel = { const groupByCategory: ViewOptionModel = {
key: 'groupBy', key: 'groupBy',
@ -565,7 +461,7 @@ export function getDefaultViewOptionsTemplatesConfig (): ViewOptionModel[] {
{ id: 'priority', label: tracker.string.Priority }, { id: 'priority', label: tracker.string.Priority },
{ id: 'project', label: tracker.string.Project }, { id: 'project', label: tracker.string.Project },
{ id: 'sprint', label: tracker.string.Sprint }, { id: 'sprint', label: tracker.string.Sprint },
{ id: 'noGrouping', label: tracker.string.NoGrouping } { id: '#no_category', label: tracker.string.NoGrouping }
], ],
type: 'dropdown' type: 'dropdown'
} }
@ -670,3 +566,28 @@ export function getTimeReportDayType (timestamp: number): TimeReportDayType | un
return TimeReportDayType.PreviousWorkDay return TimeReportDayType.PreviousWorkDay
} }
} }
export function subIssueQuery (value: boolean, query: DocumentQuery<Issue>): DocumentQuery<Issue> {
return value ? query : { ...query, attachedTo: tracker.ids.NoParent }
}
export function subIssueListProvider (subIssues: Issue[], target: Ref<Issue>): void {
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
let pos = subIssues.findIndex((p) => p._id === of?._id)
pos += offset
if (pos < 0) {
pos = 0
}
if (pos >= subIssues.length) {
pos = subIssues.length - 1
}
listProvider.updateFocus(subIssues[pos])
}
}, false)
listProvider.update(subIssues)
const selectedIssue = subIssues.find((p) => p._id === target)
if (selectedIssue != null) {
listProvider.updateFocus(selectedIssue)
}
}

View File

@ -93,7 +93,7 @@ export enum IssuesGrouping {
Priority = 'priority', Priority = 'priority',
Project = 'project', Project = 'project',
Sprint = 'sprint', Sprint = 'sprint',
NoGrouping = 'noGrouping' NoGrouping = '#no_category'
} }
/** /**

View File

@ -49,6 +49,12 @@
"MatchCriteria": "Match criteria", "MatchCriteria": "Match criteria",
"DontMatchCriteria": "Don't match criteria", "DontMatchCriteria": "Don't match criteria",
"View": "View", "View": "View",
"MarkupEditor": "Edit of rich content field" "MarkupEditor": "Edit of rich content field",
"List": "List",
"Select": "Select",
"NoGrouping": "No grouping",
"Grouping": "Grouping",
"Ordering": "Ordering",
"Manual": "Manual"
} }
} }

View File

@ -47,6 +47,12 @@
"MatchCriteria": "Соответсвует условию", "MatchCriteria": "Соответсвует условию",
"DontMatchCriteria": "Не соответвует условию", "DontMatchCriteria": "Не соответвует условию",
"View": "Вид", "View": "Вид",
"MarkupEditor": "Изменение форматированного поля" "MarkupEditor": "Изменение форматированного поля",
"List": "Список",
"Select": "Выбрать",
"NoGrouping": "Нет группировки",
"Grouping": "Группировка",
"Ordering": "Сортировка",
"Manual": "Пользовательский"
} }
} }

View File

@ -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 type { Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
export let value: Ref<Class<Doc>>
const client = getClient()
const _class = client.getModel().getObject(value)
</script>
<Label label={_class.label} />

View File

@ -21,19 +21,25 @@
export let key: string export let key: string
export let justify: string = '' export let justify: string = ''
let prevKey = key let prevKey = key
let element: HTMLDivElement | undefined
let cWidth: number = 0 let cWidth: number = 0
afterUpdate(() => { afterUpdate(() => {
if (prevKey !== key) { if (prevKey !== key) {
$fixedWidthStore[prevKey] = 0 $fixedWidthStore[prevKey] = 0
$fixedWidthStore[key] = 0
prevKey = key prevKey = key
cWidth = 0
} }
}) })
$: if (cWidth > ($fixedWidthStore[key] ?? 0)) { function resize (element: Element) {
cWidth = element.clientWidth
if (cWidth > ($fixedWidthStore[key] ?? 0)) {
$fixedWidthStore[key] = cWidth $fixedWidthStore[key] = cWidth
} }
}
onDestroy(() => { onDestroy(() => {
$fixedWidthStore[key] = 0 $fixedWidthStore[key] = 0
@ -41,13 +47,10 @@
</script> </script>
<div <div
bind:this={element}
class="flex-no-shrink" class="flex-no-shrink"
style="{justify !== '' ? `text-align: ${justify}; ` : ''} min-width: {$fixedWidthStore[key] ?? 0}px;" style="{justify !== '' ? `text-align: ${justify}; ` : ''} min-width: {$fixedWidthStore[key] ?? 0}px;"
use:resizeObserver={(element) => { use:resizeObserver={resize}
if (element.clientWidth > cWidth) {
cWidth = element.clientWidth
}
}}
> >
<slot /> <slot />
</div> </div>

View File

@ -41,7 +41,7 @@
(value as Doc)?.space === undefined (value as Doc)?.space === undefined
) { ) {
docQuery.query(value._class, { _id: value._id }, (r) => { docQuery.query(value._class, { _id: value._id }, (r) => {
doc = r.shift() ;[doc] = r
}) })
} else if (value?._id !== undefined && value?._class !== undefined && (value as Doc).space !== undefined) { } else if (value?._id !== undefined && value?._class !== undefined && (value as Doc).space !== undefined) {
docQuery.unsubscribe() docQuery.unsubscribe()

View File

@ -24,9 +24,6 @@
export let options: FindOptions<Doc> | undefined = undefined export let options: FindOptions<Doc> | undefined = undefined
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let config: string[] export let config: string[]
export let search: string = ''
$: resultQuery = search === '' ? { space, ...query } : { $search: search, space, ...query }
</script> </script>
<ActionContext <ActionContext
@ -34,4 +31,4 @@
mode: 'browser' mode: 'browser'
}} }}
/> />
<TableBrowser {_class} {config} {options} query={resultQuery} {baseMenuClass} showNotification /> <TableBrowser {_class} {config} {options} {query} {baseMenuClass} showNotification />

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Doc } from '@hcengineering/core' import { Doc } from '@hcengineering/core'
import { import ui, {
Button, Button,
IconNavPrev, IconNavPrev,
IconDownOutline, IconDownOutline,
@ -12,7 +12,6 @@
import { tick } from 'svelte' import { tick } from 'svelte'
import { select } from '../actionImpl' import { select } from '../actionImpl'
import { focusStore } from '../selection' import { focusStore } from '../selection'
import tracker from '../../../tracker-resources/src/plugin'
export let element: Doc export let element: Doc
@ -41,7 +40,7 @@
<Button icon={IconDownOutline} kind={'secondary'} size={'medium'} on:click={(evt) => next(evt, true)} /> <Button icon={IconDownOutline} kind={'secondary'} size={'medium'} on:click={(evt) => next(evt, true)} />
<Button icon={IconUpOutline} kind={'secondary'} size={'medium'} on:click={(evt) => next(evt, false)} /> <Button icon={IconUpOutline} kind={'secondary'} size={'medium'} on:click={(evt) => next(evt, false)} />
<Button <Button
showTooltip={{ label: tracker.string.Back, direction: 'bottom' }} showTooltip={{ label: ui.string.Back, direction: 'bottom' }}
icon={IconNavPrev} icon={IconNavPrev}
kind={'secondary'} kind={'secondary'}
size={'medium'} size={'medium'}

View File

@ -0,0 +1,91 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { DropdownLabelsIntl, Label, MiniToggle } from '@hcengineering/ui'
import { Viewlet, ViewOptions, ViewOptionsModel } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import view from '../plugin'
import { buildConfigLookup, getKeyLabel } from '../utils'
import { isDropdownType, isToggleType, noCategory } from '../viewOptions'
export let viewlet: Viewlet
export let config: ViewOptionsModel
export let viewOptions: ViewOptions
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config)
const groupBy = config.groupBy
.map((p) => {
return {
id: p,
label: getKeyLabel(client, viewlet.attachTo, p, lookup)
}
})
.concat({ id: noCategory, label: view.string.NoGrouping })
const orderBy = config.orderBy.map((p) => {
const key = p[0]
return {
id: key,
label: key === 'rank' ? view.string.Manual : getKeyLabel(client, viewlet.attachTo, key, lookup)
}
})
</script>
<div class="antiCard">
<div class="antiCard-group grid">
<span class="label"><Label label={view.string.Grouping} /></span>
<div class="value">
<DropdownLabelsIntl
label={view.string.Grouping}
items={groupBy}
selected={viewOptions.groupBy}
width="10rem"
justify="left"
on:selected={(e) => dispatch('update', { key: 'groupBy', value: e.detail })}
/>
</div>
<span class="label"><Label label={view.string.Ordering} /></span>
<div class="value">
<DropdownLabelsIntl
label={view.string.Ordering}
items={orderBy}
selected={viewOptions.orderBy[0]}
width="10rem"
justify="left"
on:selected={(e) => {
const key = e.detail
const value = config.orderBy.find((p) => p[0] === key)
if (value !== undefined) {
dispatch('update', { key: 'orderBy', value })
}
}}
/>
</div>
{#each config.other as model}
<span class="label"><Label label={model.label} /></span>
<div class="value">
{#if isToggleType(model)}
<MiniToggle
on={viewOptions[model.key]}
on:change={() => dispatch('update', { key: model.key, value: !viewOptions[model.key] })}
/>
{:else if isDropdownType(model)}
{@const items = model.values.filter(({ hidden }) => !hidden?.(viewOptions))}
<DropdownLabelsIntl
label={model.label}
{items}
selected={viewOptions[model.key]}
width="10rem"
justify="left"
on:selected={(e) => dispatch('update', { key: model.key, value: e.detail })}
/>
{/if}
</div>
{/each}
<slot name="extra" />
</div>
</div>

View File

@ -1,72 +0,0 @@
<!--
// 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 { Button, eventToHTMLElement, IconDownOutline, showPopup, Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
import { getViewOptions, setViewOptions, viewOptionsStore, ViewOptionModel } from '../viewOptions'
export let config: ViewOptionModel[]
export let viewOptionsKey: string
$: loadViewOptionsStore(config, viewOptionsKey)
function loadViewOptionsStore (config: ViewOptionModel[], key: string) {
viewOptionsStore.set(
config.reduce(
(options, { key, defaultValue }) => ({ [key]: defaultValue, ...options }),
getViewOptions(key) ?? {}
)
)
}
const handleOptionsEditorOpened = (event: MouseEvent) => {
showPopup(
ViewOptionsPopup,
{ config, viewOptions: $viewOptionsStore },
eventToHTMLElement(event),
undefined,
(result) => {
if (result?.key === undefined) return
$viewOptionsStore[result.key] = result.value
setViewOptions(viewOptionsKey, $viewOptionsStore)
}
)
}
</script>
<Button
icon={view.icon.ViewButton}
kind={'secondary'}
size={'small'}
showTooltip={{ label: view.string.CustomizeView }}
on:click={handleOptionsEditorOpened}
>
<svelte:fragment slot="content">
<div class="flex-row-center clear-mins pointer-events-none">
<span class="text-sm font-medium"><Label label={view.string.View} /></span>
<div class="icon"><IconDownOutline size={'full'} /></div>
</div>
</svelte:fragment>
</Button>
<style lang="scss">
.icon {
margin-left: 0.25rem;
width: 0.875rem;
height: 0.875rem;
color: var(--content-color);
}
</style>

Some files were not shown because too many files have changed in this diff Show More