mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
List extract (#2505)
This commit is contained in:
parent
1c01925ed4
commit
6a5f1ed479
@ -77,7 +77,7 @@ export class TSavedAttachments extends TPreference implements SavedAttachments {
|
||||
export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -212,15 +212,15 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -187,7 +187,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
@ -195,7 +195,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@ -190,11 +190,11 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
|
@ -48,7 +48,7 @@ import attachment from '@hcengineering/model-attachment'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@hcengineering/model-core'
|
||||
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 type { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
@ -204,7 +204,7 @@ export function createModel (builder: Builder): void {
|
||||
contact.app.Contacts
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
builder.createDoc<Viewlet>(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
@ -229,7 +229,7 @@ export function createModel (builder: Builder): void {
|
||||
pinned: true
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
builder.createDoc<Viewlet>(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
@ -237,7 +237,7 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: view.viewlet.Table,
|
||||
config: [
|
||||
'',
|
||||
'$lookup._class',
|
||||
'_class',
|
||||
'city',
|
||||
'attachments',
|
||||
'modifiedOn',
|
||||
@ -280,7 +280,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
@ -292,7 +292,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
@ -401,7 +401,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
@ -413,22 +413,38 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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, {
|
||||
actions: [view.action.Delete]
|
||||
})
|
||||
|
@ -25,6 +25,7 @@ import { Action, ActionCategory, ViewAction } from '@hcengineering/view'
|
||||
export default mergeIds(contactId, contact, {
|
||||
component: {
|
||||
PersonPresenter: '' as AnyComponent,
|
||||
ContactRefPresenter: '' as AnyComponent,
|
||||
ContactPresenter: '' as AnyComponent,
|
||||
EditPerson: '' as AnyComponent,
|
||||
EditOrganization: '' as AnyComponent,
|
||||
@ -35,6 +36,8 @@ export default mergeIds(contactId, contact, {
|
||||
EmployeeAccountPresenter: '' as AnyComponent,
|
||||
OrganizationEditor: '' as AnyComponent,
|
||||
EmployeePresenter: '' as AnyComponent,
|
||||
EmployeeRefPresenter: '' as AnyComponent,
|
||||
PersonRefPresenter: '' as AnyComponent,
|
||||
PersonEditor: '' as AnyComponent,
|
||||
Members: '' as AnyComponent,
|
||||
MemberPresenter: '' as AnyComponent,
|
||||
|
@ -224,10 +224,10 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -22,8 +22,7 @@ import { createAction } from '@hcengineering/model-view'
|
||||
import workbench from '@hcengineering/model-workbench'
|
||||
import notification from '@hcengineering/notification'
|
||||
import setting from '@hcengineering/setting'
|
||||
import type {} from '@hcengineering/view'
|
||||
import view from '@hcengineering/view'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import inventory from './plugin'
|
||||
|
||||
export const DOMAIN_INVENTORY = 'inventory' as Domain
|
||||
@ -75,15 +74,19 @@ export class TVariant extends TAttachedDoc implements Variant {
|
||||
export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
@ -99,13 +102,13 @@ export function createModel (builder: Builder): void {
|
||||
value: true
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
builder.createDoc<Viewlet>(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: inventory.class.Product,
|
||||
descriptor: view.viewlet.Table,
|
||||
config: ['', '$lookup.attachedTo', 'modifiedOn']
|
||||
config: ['', 'attachedTo', 'modifiedOn']
|
||||
},
|
||||
inventory.viewlet.TableProduct
|
||||
)
|
||||
|
@ -36,6 +36,7 @@ export default mergeIds(inventoryId, inventory, {
|
||||
CreateProduct: '' as AnyComponent,
|
||||
EditProduct: '' as AnyComponent,
|
||||
CategoryPresenter: '' as AnyComponent,
|
||||
CategoryRefPresenter: '' as AnyComponent,
|
||||
Variants: '' as AnyComponent,
|
||||
ProductPresenter: '' as AnyComponent,
|
||||
VariantPresenter: '' as AnyComponent
|
||||
|
@ -189,7 +189,7 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: view.viewlet.Table,
|
||||
config: [
|
||||
'',
|
||||
'$lookup._class',
|
||||
'_class',
|
||||
'leads',
|
||||
'modifiedOn',
|
||||
{
|
||||
@ -212,9 +212,9 @@ export function createModel (builder: Builder): void {
|
||||
config: [
|
||||
'',
|
||||
'title',
|
||||
'$lookup.attachedTo',
|
||||
'$lookup.state',
|
||||
'$lookup.doneState',
|
||||
'attachedTo',
|
||||
'state',
|
||||
'doneState',
|
||||
'attachments',
|
||||
'comments',
|
||||
'modifiedOn',
|
||||
@ -227,6 +227,36 @@ export function createModel (builder: Builder): void {
|
||||
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, {
|
||||
attachTo: lead.class.Lead,
|
||||
descriptor: task.viewlet.Kanban,
|
||||
@ -254,7 +284,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -30,7 +30,6 @@ export default mergeIds(leadId, lead, {
|
||||
LeadApplication: '' as IntlString,
|
||||
Lead: '' as IntlString,
|
||||
Title: '' as IntlString,
|
||||
Assignee: '' as IntlString,
|
||||
ManageFunnelStatuses: '' as IntlString,
|
||||
FunnelBrowser: '' as IntlString,
|
||||
GotoLeadApplication: '' as IntlString
|
||||
@ -52,7 +51,8 @@ export default mergeIds(leadId, lead, {
|
||||
},
|
||||
viewlet: {
|
||||
TableCustomer: '' as Ref<Viewlet>,
|
||||
TableLead: '' as Ref<Viewlet>
|
||||
TableLead: '' as Ref<Viewlet>,
|
||||
ListLead: '' as Ref<Viewlet>
|
||||
},
|
||||
category: {
|
||||
Lead: '' as Ref<ActionCategory>
|
||||
|
@ -351,10 +351,10 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: task.viewlet.StatusTable,
|
||||
config: [
|
||||
'',
|
||||
'$lookup.attachedTo',
|
||||
'$lookup.assignee',
|
||||
'$lookup.state',
|
||||
'$lookup.doneState',
|
||||
'attachedTo',
|
||||
'assignee',
|
||||
'state',
|
||||
'doneState',
|
||||
'attachments',
|
||||
'comments',
|
||||
'modifiedOn',
|
||||
@ -415,7 +415,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
@ -423,7 +423,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -60,11 +60,11 @@ export function createReviewModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -56,7 +56,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -106,10 +106,10 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -366,29 +366,20 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
attachTo: task.class.Issue,
|
||||
descriptor: task.viewlet.StatusTable,
|
||||
config: [
|
||||
'',
|
||||
'name',
|
||||
'$lookup.assignee',
|
||||
'$lookup.state',
|
||||
'$lookup.doneState',
|
||||
'attachments',
|
||||
'comments',
|
||||
'modifiedOn'
|
||||
]
|
||||
config: ['', 'name', 'assignee', 'state', 'doneState', 'attachments', 'comments', 'modifiedOn']
|
||||
},
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
@ -462,10 +453,14 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
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, {
|
||||
actions: [view.action.Delete]
|
||||
})
|
||||
@ -474,10 +469,14 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
builder.mixin(task.class.DoneState, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: task.component.DoneStateRefPresenter
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ViewletDescriptor,
|
||||
core.space.Model,
|
||||
@ -504,7 +503,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -58,7 +58,9 @@ export default mergeIds(taskId, task, {
|
||||
TodoItemPresenter: '' as AnyComponent,
|
||||
StatusTableView: '' as AnyComponent,
|
||||
TaskHeader: '' as AnyComponent,
|
||||
Dashboard: '' as AnyComponent
|
||||
Dashboard: '' as AnyComponent,
|
||||
StateRefPresenter: '' as AnyComponent,
|
||||
DoneStateRefPresenter: '' as AnyComponent
|
||||
},
|
||||
space: {
|
||||
TasksPublic: '' as Ref<Space>
|
||||
|
@ -73,7 +73,7 @@ import {
|
||||
trackerId,
|
||||
WorkDayLength
|
||||
} from '@hcengineering/tracker'
|
||||
import { KeyBinding } from '@hcengineering/view'
|
||||
import { KeyBinding, ViewOptionsModel } from '@hcengineering/view'
|
||||
import tracker from './plugin'
|
||||
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
@ -467,9 +467,31 @@ export function createModel (builder: Builder): void {
|
||||
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, {
|
||||
attachTo: tracker.class.Issue,
|
||||
descriptor: tracker.viewlet.List,
|
||||
descriptor: view.viewlet.List,
|
||||
viewOptions: issuesOptions,
|
||||
config: [
|
||||
{
|
||||
key: '',
|
||||
@ -484,7 +506,7 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
|
||||
{ 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: '',
|
||||
@ -530,11 +552,21 @@ export function createModel (builder: Builder): void {
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
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: [
|
||||
// { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } },
|
||||
{ key: '', presenter: tracker.component.IssueTemplatePresenter, props: { type: 'issue', shouldUseMargin: true } },
|
||||
{ key: '', presenter: tracker.component.GrowPresenter, props: { type: 'grow' } },
|
||||
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
|
||||
// { key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
|
||||
{
|
||||
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, {
|
||||
attachTo: tracker.class.Issue,
|
||||
descriptor: tracker.viewlet.Kanban,
|
||||
viewOptions: issuesOptions,
|
||||
config: []
|
||||
})
|
||||
|
||||
@ -657,11 +679,11 @@ export function createModel (builder: Builder): void {
|
||||
const sprintsId = 'sprints'
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
@ -669,7 +691,7 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
@ -677,7 +699,23 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
@ -685,33 +723,40 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
classPresenter(
|
||||
builder,
|
||||
tracker.class.Project,
|
||||
tracker.component.ProjectTitlePresenter,
|
||||
tracker.component.ProjectSelector
|
||||
)
|
||||
classPresenter(builder, tracker.class.Project, tracker.component.ProjectSelector, tracker.component.ProjectSelector)
|
||||
|
||||
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.AttributeEditor, {
|
||||
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
|
||||
})
|
||||
|
||||
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, {
|
||||
value: true
|
||||
})
|
||||
|
@ -41,7 +41,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
// Required to pass build without errorsF
|
||||
Nope: '' as AnyComponent,
|
||||
SprintSelector: '' as AnyComponent,
|
||||
SubIssuesSelector: '' as AnyComponent
|
||||
SubIssuesSelector: '' as AnyComponent,
|
||||
IssueStatistics: '' as AnyComponent
|
||||
},
|
||||
app: {
|
||||
Tracker: '' as Ref<Application>
|
||||
|
@ -29,6 +29,7 @@ import type {
|
||||
AttributePresenter,
|
||||
BuildModelKey,
|
||||
ClassFilters,
|
||||
ClassSortFuncs,
|
||||
CollectionEditor,
|
||||
CollectionPresenter,
|
||||
Filter,
|
||||
@ -38,13 +39,16 @@ import type {
|
||||
KeyBinding,
|
||||
KeyFilter,
|
||||
LinkPresenter,
|
||||
ListHeaderExtra,
|
||||
ListItemPresenter,
|
||||
ObjectEditor,
|
||||
ObjectEditorHeader,
|
||||
ObjectFactory,
|
||||
ObjectPresenter,
|
||||
ObjectTitle,
|
||||
ObjectValidator,
|
||||
PreviewPresenter,
|
||||
ListItemPresenter,
|
||||
SortFunc,
|
||||
SpaceHeader,
|
||||
SpaceName,
|
||||
ViewAction,
|
||||
@ -52,7 +56,8 @@ import type {
|
||||
ViewContext,
|
||||
Viewlet,
|
||||
ViewletDescriptor,
|
||||
ViewletPreference
|
||||
ViewletPreference,
|
||||
ViewOptionsModel
|
||||
} from '@hcengineering/view'
|
||||
import view from './plugin'
|
||||
|
||||
@ -134,6 +139,11 @@ export class TAttributePresenter extends TClass implements AttributePresenter {
|
||||
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)
|
||||
export class TListItemPresenter extends TClass implements ListItemPresenter {
|
||||
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>>
|
||||
}
|
||||
|
||||
@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)
|
||||
export class TViewletPreference extends TPreference implements ViewletPreference {
|
||||
attachedTo!: Ref<Viewlet>
|
||||
@ -190,11 +210,12 @@ export class TViewletDescriptor extends TDoc implements ViewletDescriptor {
|
||||
|
||||
@Model(view.class.Viewlet, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TViewlet extends TDoc implements Viewlet {
|
||||
attachTo!: Ref<Class<Space>>
|
||||
attachTo!: Ref<Class<Doc>>
|
||||
descriptor!: Ref<ViewletDescriptor>
|
||||
open!: AnyComponent
|
||||
config!: (BuildModelKey | string)[]
|
||||
hiddenKeys?: string[]
|
||||
viewOptions?: ViewOptionsModel
|
||||
}
|
||||
|
||||
@Model(view.class.Action, core.class.Doc, DOMAIN_MODEL)
|
||||
@ -279,6 +300,9 @@ export function createModel (builder: Builder): void {
|
||||
TCollectionEditor,
|
||||
TCollectionPresenter,
|
||||
TObjectEditor,
|
||||
TObjectPresenter,
|
||||
TSortFuncs,
|
||||
TListHeaderExtra,
|
||||
TViewletPreference,
|
||||
TViewletDescriptor,
|
||||
TViewlet,
|
||||
@ -330,7 +354,14 @@ export function createModel (builder: Builder): void {
|
||||
classPresenter(builder, core.class.TypeTimestamp, view.component.TimestampPresenter)
|
||||
classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor)
|
||||
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)
|
||||
|
||||
@ -376,6 +407,17 @@ export function createModel (builder: Builder): void {
|
||||
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(
|
||||
builder,
|
||||
{
|
||||
|
@ -58,10 +58,13 @@ export default mergeIds(viewId, view, {
|
||||
YoutubePresenter: '' as AnyComponent,
|
||||
GithubPresenter: '' as AnyComponent,
|
||||
ClassPresenter: '' as AnyComponent,
|
||||
ClassRefPresenter: '' as AnyComponent,
|
||||
EnumEditor: '' as AnyComponent,
|
||||
HTMLEditor: '' as AnyComponent,
|
||||
MarkupEditor: '' as AnyComponent,
|
||||
MarkupEditorPopup: '' as AnyComponent
|
||||
MarkupEditorPopup: '' as AnyComponent,
|
||||
ListView: '' as AnyComponent,
|
||||
GrowPresenter: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Table: '' as IntlString,
|
||||
@ -84,7 +87,8 @@ export default mergeIds(viewId, view, {
|
||||
General: '' as IntlString,
|
||||
Navigation: '' as IntlString,
|
||||
Editor: '' as IntlString,
|
||||
MarkdownFormatting: '' as IntlString
|
||||
MarkdownFormatting: '' as IntlString,
|
||||
List: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
FilterObjectInResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise<ObjQueryType<any>>>,
|
||||
|
@ -48,7 +48,7 @@ export class TSpaceView extends TClass implements SpaceView {
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 { getPlatformColor, ScrollBox, Scroller } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -22,14 +22,13 @@
|
||||
import KanbanRow from './KanbanRow.svelte'
|
||||
|
||||
export let _class: Ref<Class<Item>>
|
||||
export let search: string
|
||||
export let options: FindOptions<Item> | undefined = undefined
|
||||
export let states: TypeState[] = []
|
||||
export let query: DocumentQuery<Item> = {}
|
||||
export let fieldName: string
|
||||
export let rankFieldName: string | undefined
|
||||
export let selection: number | undefined = undefined
|
||||
export let checked: Doc[] = []
|
||||
export let dontUpdateRank: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -38,15 +37,13 @@
|
||||
const objsQ = createQuery()
|
||||
$: objsQ.query(
|
||||
_class,
|
||||
{
|
||||
...query,
|
||||
...(search !== '' ? { $search: search } : {})
|
||||
},
|
||||
query,
|
||||
(result) => {
|
||||
objects = result
|
||||
dispatch('content', objects)
|
||||
},
|
||||
{
|
||||
sort: { rank: SortingOrder.Ascending },
|
||||
...options
|
||||
}
|
||||
)
|
||||
@ -57,10 +54,6 @@
|
||||
dragItem?: Item // required for svelte to properly recalculate state.
|
||||
): ExtItem[] {
|
||||
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) => ({
|
||||
it,
|
||||
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) {
|
||||
if (dragCard === undefined) {
|
||||
return
|
||||
@ -99,15 +75,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (rankFieldName !== undefined && dragCardInitialRank !== (dragCard as any)[rankFieldName]) {
|
||||
const dragCardRank = (dragCard as any)[rankFieldName]
|
||||
if (!dontUpdateRank && dragCardInitialRank !== dragCard.rank) {
|
||||
const dragCardRank = dragCard.rank
|
||||
updates = {
|
||||
...updates,
|
||||
[rankFieldName]: dragCardRank
|
||||
rank: dragCardRank
|
||||
}
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await updateItem(dragCard, updates)
|
||||
await client.update(dragCard, updates)
|
||||
}
|
||||
dragCard = undefined
|
||||
}
|
||||
@ -125,7 +101,7 @@
|
||||
if (dragCard === undefined) {
|
||||
return
|
||||
}
|
||||
await updateItem(dragCard, query)
|
||||
await client.update(dragCard, query)
|
||||
}
|
||||
function doCalcRank (
|
||||
object: { prev?: Item; it: Item; next?: Item },
|
||||
@ -146,26 +122,28 @@
|
||||
if (card !== undefined && card[fieldName] !== state._id) {
|
||||
card[fieldName] = state._id
|
||||
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 {
|
||||
if (dragCard !== undefined) {
|
||||
;(dragCard as any)[fieldName] = (dragCard as any)[fieldName]
|
||||
if (rankFieldName !== undefined) {
|
||||
;(dragCard as any)[rankFieldName] = doCalcRank(object, evt)
|
||||
if (!dontUpdateRank) {
|
||||
dragCard.rank = doCalcRank(object, evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
function cardDrop (evt: CardDragEvent, object: ExtItem): void {
|
||||
if (dragCard !== undefined && rankFieldName !== undefined) {
|
||||
;(dragCard as any)[rankFieldName] = doCalcRank(object, evt)
|
||||
if (!dontUpdateRank && dragCard !== undefined) {
|
||||
dragCard.rank = doCalcRank(object, evt)
|
||||
}
|
||||
isDragging = false
|
||||
}
|
||||
function onDragStart (object: ExtItem, state: TypeState): void {
|
||||
dragCardInitialState = state._id
|
||||
dragCardInitialRank = rankFieldName === undefined ? undefined : (object.it as any)[rankFieldName]
|
||||
dragCardInitialRank = object.it.rank
|
||||
dragCard = object.it
|
||||
isDragging = true
|
||||
dispatch('obj-focus', object.it)
|
||||
|
@ -27,6 +27,7 @@
|
||||
"Add": "Add",
|
||||
"Edit": "Edit",
|
||||
"SelectAvatar": "Select avatar",
|
||||
"GravatarsManaged": "Gravatars are managed through"
|
||||
"GravatarsManaged": "Gravatars are managed through",
|
||||
"InltPropsValue": "{value}"
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@
|
||||
"Add": "Добавить",
|
||||
"Edit": "Редактировать",
|
||||
"SelectAvatar": "Выбрать аватар",
|
||||
"GravatarsManaged": "Граватары управляются через"
|
||||
"GravatarsManaged": "Граватары управляются через",
|
||||
"InltPropsValue": "{value}"
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,8 @@ export default plugin(presentationId, {
|
||||
Add: '' as IntlString,
|
||||
Edit: '' as IntlString,
|
||||
SelectAvatar: '' as IntlString,
|
||||
GravatarsManaged: '' as IntlString
|
||||
GravatarsManaged: '' as IntlString,
|
||||
InltPropsValue: '' as IntlString
|
||||
},
|
||||
metadata: {
|
||||
RequiredVersion: '' as Metadata<string>,
|
||||
|
@ -200,7 +200,7 @@ export async function copyTextToClipboard (text: string): Promise<void> {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AttributeCategory = 'attribute' | 'inplace' | 'collection' | 'array'
|
||||
export type AttributeCategory = 'object' | 'attribute' | 'inplace' | 'collection' | 'array'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -215,7 +215,7 @@ export function getAttributePresenterClass (
|
||||
attribute: AnyAttribute
|
||||
): { attrClass: Ref<Class<Doc>>, category: AttributeCategory } {
|
||||
let attrClass = attribute.type._class
|
||||
let category: AttributeCategory = 'attribute'
|
||||
let category: AttributeCategory = 'object'
|
||||
if (hierarchy.isDerived(attrClass, core.class.RefTo)) {
|
||||
attrClass = (attribute.type as RefTo<Doc>).to
|
||||
category = 'attribute'
|
||||
|
@ -32,7 +32,7 @@
|
||||
|
||||
let time: string = ''
|
||||
|
||||
async function formatTime (now: number) {
|
||||
async function formatTime (now: number, value: number) {
|
||||
let passed = now - value
|
||||
if (passed < 0) passed = 0
|
||||
if (passed < HOUR) {
|
||||
@ -48,7 +48,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: formatTime($ticker)
|
||||
$: formatTime($ticker, value)
|
||||
|
||||
$: tooltipValue = new Date(value).toLocaleString('default', {
|
||||
minute: '2-digit',
|
||||
|
@ -15,7 +15,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import type { Kanban, SpaceWithStates, State } from '@hcengineering/task'
|
||||
@ -37,7 +37,7 @@
|
||||
|
||||
export let _class: Ref<Class<Card>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let search: string
|
||||
export let query: DocumentQuery<Card>
|
||||
export let options: FindOptions<Card> | undefined
|
||||
|
||||
let kanban: Kanban
|
||||
@ -95,6 +95,8 @@
|
||||
|
||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
$: resultQuery = { ...query, doneState: null, isArchived: { $nin: [true] }, space }
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@ -105,12 +107,10 @@
|
||||
<KanbanUI
|
||||
bind:this={kanbanUI}
|
||||
{_class}
|
||||
{search}
|
||||
{options}
|
||||
query={{ doneState: null, isArchived: { $nin: [true] }, space }}
|
||||
query={resultQuery}
|
||||
{states}
|
||||
fieldName={'state'}
|
||||
rankFieldName={'rank'}
|
||||
on:content={(evt) => {
|
||||
listProvider.update(evt.detail)
|
||||
}}
|
||||
|
@ -23,7 +23,7 @@
|
||||
{_class}
|
||||
config={[
|
||||
'title',
|
||||
'$lookup.state',
|
||||
'state',
|
||||
{
|
||||
key: '',
|
||||
presenter: tags.component.TagsPresenter,
|
||||
|
@ -40,7 +40,6 @@
|
||||
export let options: FindOptions<Event> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Event>> | undefined = undefined
|
||||
export let config: (string | BuildModelKey)[]
|
||||
export let search: string = ''
|
||||
export let createComponent: AnyComponent | undefined = undefined
|
||||
|
||||
const mondayStart = true
|
||||
@ -49,10 +48,6 @@
|
||||
let currentDate: 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[] = []
|
||||
|
||||
const q = createQuery()
|
||||
@ -67,7 +62,7 @@
|
||||
{ sort: { date: SortingOrder.Ascending }, ...options }
|
||||
)
|
||||
}
|
||||
$: update(_class, resultQuery, options)
|
||||
$: update(_class, query, options)
|
||||
|
||||
function areDatesLess (firstDate: Date, secondDate: Date): boolean {
|
||||
return (
|
||||
@ -256,7 +251,7 @@
|
||||
{today}
|
||||
{selected}
|
||||
{wrongMonth}
|
||||
query={resultQuery}
|
||||
{query}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</YearCalendar>
|
||||
@ -276,7 +271,7 @@
|
||||
{today}
|
||||
{selected}
|
||||
{wrongMonth}
|
||||
query={resultQuery}
|
||||
{query}
|
||||
on:select={(e) => {
|
||||
currentDate = e.detail
|
||||
if (areDatesEqual(selectedDate, currentDate)) {
|
||||
|
@ -72,6 +72,7 @@
|
||||
"Birthday": "Birthday",
|
||||
"UseImage": "Upload an image",
|
||||
"UseGravatar": "Use Gravatar",
|
||||
"UseColor": "Use color"
|
||||
"UseColor": "Use color",
|
||||
"NotSpecified": "Not specified"
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@
|
||||
"Birthday": "День рождения",
|
||||
"UseImage": "Загрузить фото",
|
||||
"UseGravatar": "Использовать Gravatar",
|
||||
"UseColor": "Использовать цвет"
|
||||
"UseColor": "Использовать цвет",
|
||||
"NotSpecified": "Не указан"
|
||||
}
|
||||
}
|
@ -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}
|
@ -2,21 +2,14 @@
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import type { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import { PersonLabelTooltip } from '..'
|
||||
import PersonPresenter from '../components/PersonPresenter.svelte'
|
||||
import EmployeePreviewPopup from './EmployeePreviewPopup.svelte'
|
||||
import EmployeeStatusPresenter from './EmployeeStatusPresenter.svelte'
|
||||
|
||||
export let value: WithLookup<Employee> | null | undefined
|
||||
export let tooltipLabels:
|
||||
| {
|
||||
personLabel?: IntlString
|
||||
placeholderLabel?: IntlString
|
||||
component?: AnySvelteComponent | AnyComponent
|
||||
props?: any
|
||||
}
|
||||
| undefined = undefined
|
||||
export let tooltipLabels: PersonLabelTooltip | undefined = undefined
|
||||
export let shouldShowAvatar: boolean = true
|
||||
export let shouldShowName: boolean = true
|
||||
export let shouldShowPlaceholder = false
|
||||
@ -25,6 +18,7 @@
|
||||
export let isInteractive = true
|
||||
export let inline = false
|
||||
export let disableClick = false
|
||||
export let defaultName: IntlString | undefined = undefined
|
||||
|
||||
let container: HTMLElement
|
||||
|
||||
@ -48,7 +42,7 @@
|
||||
$: handlePersonEdit = onEmployeeEdit ?? onEdit
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class:over-underline={!inline}>
|
||||
<div bind:this={container}>
|
||||
<PersonPresenter
|
||||
{value}
|
||||
{tooltipLabels}
|
||||
@ -59,6 +53,7 @@
|
||||
{shouldShowPlaceholder}
|
||||
{isInteractive}
|
||||
{inline}
|
||||
{defaultName}
|
||||
/>
|
||||
</div>
|
||||
{#if value?.$lookup?.statuses?.length}
|
||||
|
@ -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
|
||||
/>
|
@ -15,8 +15,10 @@
|
||||
<script lang="ts">
|
||||
import { formatName, Person } from '@hcengineering/contact'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { LabelAndProps } from '@hcengineering/ui'
|
||||
import { PersonLabelTooltip } from '..'
|
||||
import PersonContent from './PersonContent.svelte'
|
||||
import type { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
||||
|
||||
export let value: Person | null | undefined
|
||||
export let inline = false
|
||||
@ -26,27 +28,40 @@
|
||||
export let shouldShowName = true
|
||||
export let shouldShowPlaceholder = false
|
||||
export let defaultName: IntlString | undefined = undefined
|
||||
export let tooltipLabels:
|
||||
| {
|
||||
personLabel?: IntlString
|
||||
placeholderLabel?: IntlString
|
||||
component?: AnySvelteComponent | AnyComponent
|
||||
props?: any
|
||||
}
|
||||
| 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
|
||||
|
||||
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>
|
||||
|
||||
{#if value || shouldShowPlaceholder}
|
||||
<PersonContent
|
||||
showTooltip={tooltipLabels
|
||||
? {
|
||||
label: value ? tooltipLabels.personLabel : undefined,
|
||||
component: value ? tooltipLabels.component : undefined,
|
||||
props: value && tooltipLabels.personLabel ? { value: formatName(value.name) } : tooltipLabels.props
|
||||
}
|
||||
: undefined}
|
||||
showTooltip={getTooltip(tooltipLabels, value)}
|
||||
{value}
|
||||
{inline}
|
||||
{onEdit}
|
||||
|
@ -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}
|
||||
/>
|
@ -17,9 +17,9 @@
|
||||
import { Channel, Contact, Employee, formatName, getGravatarUrl } from '@hcengineering/contact'
|
||||
import { Class, Client, DocumentQuery, Ref, RelatedDocument, WithLookup } from '@hcengineering/core'
|
||||
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 { showPopup } from '@hcengineering/ui'
|
||||
import { AnyComponent, AnySvelteComponent, showPopup } from '@hcengineering/ui'
|
||||
import Channels from './components/Channels.svelte'
|
||||
import ChannelsDropdown from './components/ChannelsDropdown.svelte'
|
||||
import ChannelsEditor from './components/ChannelsEditor.svelte'
|
||||
@ -48,7 +48,11 @@ import OrganizationPresenter from './components/OrganizationPresenter.svelte'
|
||||
import PersonEditor from './components/PersonEditor.svelte'
|
||||
import PersonPresenter from './components/PersonPresenter.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 { employeeSort } from './utils'
|
||||
|
||||
export {
|
||||
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> => ({
|
||||
actionImpl: {
|
||||
KickEmployee: kickEmployee,
|
||||
@ -126,6 +137,8 @@ export default async (): Promise<Resources> => ({
|
||||
PersonEditor,
|
||||
OrganizationEditor,
|
||||
ContactPresenter,
|
||||
ContactRefPresenter,
|
||||
PersonRefPresenter,
|
||||
PersonPresenter,
|
||||
OrganizationPresenter,
|
||||
ChannelsPresenter,
|
||||
@ -139,6 +152,7 @@ export default async (): Promise<Resources> => ({
|
||||
Contacts,
|
||||
EmployeeAccountPresenter,
|
||||
EmployeePresenter,
|
||||
EmployeeRefPresenter,
|
||||
Members,
|
||||
MemberPresenter,
|
||||
EditMember,
|
||||
@ -164,6 +178,7 @@ export default async (): Promise<Resources> => ({
|
||||
function: {
|
||||
GetFileUrl: getFileUrl,
|
||||
GetGravatarUrl: getGravatarUrl,
|
||||
GetColorUrl: (uri: string) => uri
|
||||
GetColorUrl: (uri: string) => uri,
|
||||
EmployeeSort: employeeSort
|
||||
}
|
||||
})
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import contact, { contactId } from '@hcengineering/contact'
|
||||
import { IntlString, mergeIds } from '@hcengineering/platform'
|
||||
import { SortFunc } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(contactId, contact, {
|
||||
string: {
|
||||
@ -61,6 +62,10 @@ export default mergeIds(contactId, contact, {
|
||||
KickEmployeeDescr: '' as IntlString,
|
||||
Email: '' as IntlString,
|
||||
CreateEmployee: '' as IntlString,
|
||||
Inactive: '' as IntlString
|
||||
Inactive: '' as IntlString,
|
||||
NotSpecified: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
EmployeeSort: '' as SortFunc
|
||||
}
|
||||
})
|
||||
|
@ -14,9 +14,9 @@
|
||||
// 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 { getClient } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
|
||||
const client = getClient()
|
||||
const channelProviders = client.findAll(contact.class.ChannelProvider, {})
|
||||
@ -38,3 +38,35 @@ export function formatDate (dueDateMs: Timestamp): string {
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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}
|
@ -19,6 +19,7 @@ import { Resources } from '@hcengineering/platform'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import Categories from './components/Categories.svelte'
|
||||
import CategoryPresenter from './components/CategoryPresenter.svelte'
|
||||
import CategoryRefPresenter from './components/CategoryRefPresenter.svelte'
|
||||
import CreateCategory from './components/CreateCategory.svelte'
|
||||
import EditProduct from './components/EditProduct.svelte'
|
||||
import ProductPresenter from './components/ProductPresenter.svelte'
|
||||
@ -37,6 +38,7 @@ export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
Categories,
|
||||
CategoryPresenter,
|
||||
CategoryRefPresenter,
|
||||
ProductPresenter,
|
||||
EditProduct,
|
||||
Variants,
|
||||
|
@ -30,6 +30,7 @@
|
||||
"Description": "Description",
|
||||
"FullDescription": "Full description",
|
||||
"FunnelPlaceholder": "The simple funnel",
|
||||
"Members": "Members"
|
||||
"Members": "Members",
|
||||
"UnAssign": "Unassign"
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
"Leads": "Сделки",
|
||||
"SelectCustomer": "Выбрать клиента",
|
||||
"Lead": "Сделка",
|
||||
"Assignee": "Назначена",
|
||||
"Assignee": "Исполнитель",
|
||||
"Title": "Загаловок",
|
||||
"LeadPlaceholder": "Простая сделка",
|
||||
"ManageFunnelStatuses": "Управление статусами воронки",
|
||||
@ -30,6 +30,7 @@
|
||||
"Description": "Описание",
|
||||
"FullDescription": "Детальное описание",
|
||||
"FunnelPlaceholder": "Простая воронка",
|
||||
"Members": "Пользователи"
|
||||
"Members": "Пользователи",
|
||||
"UnAssign": "Отменить назначение"
|
||||
}
|
||||
}
|
@ -14,11 +14,11 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 type { Customer, Lead } from '@hcengineering/lead'
|
||||
import type { Customer, Funnel, Lead } from '@hcengineering/lead'
|
||||
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 { createFocusManager, EditBox, FocusHandler, Label, Status as StatusControl, Button } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -41,14 +41,20 @@
|
||||
return (preserveCustomer || customer === undefined) && title === ''
|
||||
}
|
||||
|
||||
$: client.findAll(lead.class.Funnel, {}).then((r) => {
|
||||
if (r.find((it) => it._id === _space) === undefined) {
|
||||
_space = r.shift()?._id as Ref<Space>
|
||||
let funnels: Funnel[] = []
|
||||
const funnelQuery = createQuery()
|
||||
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 () {
|
||||
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) {
|
||||
throw new Error('create application: state not found')
|
||||
}
|
||||
@ -145,6 +151,13 @@
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="pool">
|
||||
<EmployeeBox
|
||||
focusIndex={2}
|
||||
label={lead.string.Assignee}
|
||||
bind:value={assignee}
|
||||
allowDeselect
|
||||
titleDeselect={lead.string.UnAssign}
|
||||
/>
|
||||
{#if !preserveCustomer}
|
||||
<UserBox
|
||||
focusIndex={2}
|
||||
|
@ -42,7 +42,7 @@
|
||||
<Scroller horizontal>
|
||||
<Table
|
||||
_class={lead.class.Lead}
|
||||
config={['', '$lookup.state', '$lookup.doneState']}
|
||||
config={['', 'state', 'doneState']}
|
||||
query={{ attachedTo: objectId }}
|
||||
{loadingProps}
|
||||
/>
|
||||
@ -50,7 +50,7 @@
|
||||
{:else}
|
||||
<Table
|
||||
_class={lead.class.Lead}
|
||||
config={['', '$lookup.state', '$lookup.doneState']}
|
||||
config={['', 'state', 'doneState']}
|
||||
query={{ attachedTo: objectId }}
|
||||
{loadingProps}
|
||||
/>
|
||||
|
@ -21,8 +21,4 @@
|
||||
export let value: Customer
|
||||
</script>
|
||||
|
||||
<Table
|
||||
_class={leads.class.Lead}
|
||||
config={['', '$lookup.state', '$lookup.doneState']}
|
||||
query={{ attachedTo: value._id }}
|
||||
/>
|
||||
<Table _class={leads.class.Lead} config={['', 'state', 'doneState']} query={{ attachedTo: value._id }} />
|
||||
|
@ -41,7 +41,9 @@ export default mergeIds(leadId, lead, {
|
||||
Description: '' as IntlString,
|
||||
FullDescription: '' as IntlString,
|
||||
FunnelPlaceholder: '' as IntlString,
|
||||
Members: '' as IntlString
|
||||
Members: '' as IntlString,
|
||||
Assignee: '' as IntlString,
|
||||
UnAssign: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
CreateCustomer: '' as AnyComponent,
|
||||
|
@ -35,8 +35,8 @@
|
||||
'',
|
||||
'$lookup.space.name',
|
||||
'$lookup.space.$lookup.company',
|
||||
'$lookup.state',
|
||||
'$lookup.doneState'
|
||||
'state',
|
||||
'doneState'
|
||||
]
|
||||
let wSection: number
|
||||
</script>
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
<Table
|
||||
_class={recruit.class.Applicant}
|
||||
config={['', '$lookup.space.name', '$lookup.state', '$lookup.doneState']}
|
||||
config={['', '$lookup.space.name', 'state', 'doneState']}
|
||||
query={{ attachedTo: value._id }}
|
||||
loadingProps={{ length: value.applications ?? 0 }}
|
||||
/>
|
||||
|
@ -35,7 +35,7 @@
|
||||
<div class="popup-table">
|
||||
<Table
|
||||
_class={recruit.class.Applicant}
|
||||
config={['', '$lookup.attachedTo', '$lookup.state', '$lookup.doneState', 'modifiedOn']}
|
||||
config={['', 'attachedTo', 'state', 'doneState', 'modifiedOn']}
|
||||
{options}
|
||||
query={{ ...(resultQuery ?? {}), space: value }}
|
||||
loadingProps={{ length: 0 }}
|
||||
|
@ -88,16 +88,7 @@
|
||||
|
||||
<TableBrowser
|
||||
{_class}
|
||||
config={[
|
||||
'',
|
||||
'$lookup.attachedTo',
|
||||
'$lookup.assignee',
|
||||
'$lookup.state',
|
||||
'$lookup.doneState',
|
||||
'attachments',
|
||||
'comments',
|
||||
'modifiedOn'
|
||||
]}
|
||||
config={['', 'attachedTo', 'assignee', 'state', 'doneState', 'attachments', 'comments', 'modifiedOn']}
|
||||
query={resultQuery}
|
||||
showNotification
|
||||
/>
|
||||
|
@ -18,11 +18,11 @@
|
||||
import type { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task'
|
||||
import task from '@hcengineering/task'
|
||||
import { BarDashboard, DashboardItem } from '@hcengineering/ui'
|
||||
import { FilterBar } from '@hcengineering/view-resources'
|
||||
import CreateFilter from './CreateFilter.svelte'
|
||||
|
||||
export let _class: Ref<Class<Task>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let query: DocumentQuery<Task>
|
||||
|
||||
const client = getClient()
|
||||
const hieararchy = client.getHierarchy()
|
||||
@ -85,15 +85,12 @@
|
||||
|
||||
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 {
|
||||
if (states.length === 0) {
|
||||
@ -146,7 +143,6 @@
|
||||
</script>
|
||||
|
||||
<CreateFilter bind:value={modified} />
|
||||
<FilterBar {_class} {query} on:change={(e) => (resultQuery = e.detail)} />
|
||||
|
||||
<div class="ml-10 mt-4">
|
||||
<BarDashboard {items} />
|
||||
|
@ -15,7 +15,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 { TabList } from '@hcengineering/ui'
|
||||
import type { TabItem } from '@hcengineering/ui'
|
||||
@ -27,26 +27,26 @@
|
||||
|
||||
export let _class: Ref<Class<Task>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let query: DocumentQuery<Task>
|
||||
export let options: FindOptions<Task> | undefined
|
||||
export let config: string[]
|
||||
export let search: string
|
||||
|
||||
let doneStatusesView: boolean = false
|
||||
let state: Ref<State> | undefined = undefined
|
||||
const selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
|
||||
$: resConfig = updateConfig(config)
|
||||
let query = {}
|
||||
let doneStates: DoneState[] = []
|
||||
let itemsDS: TabItem[] = []
|
||||
let selectedDS: string[] = []
|
||||
let withoutDone: boolean = false
|
||||
let resultQuery: DocumentQuery<Task>
|
||||
|
||||
function updateConfig (config: string[]): string[] {
|
||||
if (state !== undefined) {
|
||||
return config.filter((p) => p !== '$lookup.state')
|
||||
return config.filter((p) => p !== 'state')
|
||||
}
|
||||
if (selectedDoneStates.size === 1) {
|
||||
return config.filter((p) => p !== '$lookup.doneState')
|
||||
return config.filter((p) => p !== 'doneState')
|
||||
}
|
||||
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)
|
||||
const result = {} as DocumentQuery<Task>
|
||||
if (search !== '') {
|
||||
result.$search = search
|
||||
}
|
||||
const result = client.getHierarchy().clone(query)
|
||||
result.space = space
|
||||
if (state) {
|
||||
result.state = state
|
||||
@ -94,7 +93,7 @@
|
||||
} else if (withoutDone) {
|
||||
result.doneState = null
|
||||
}
|
||||
query = result
|
||||
resultQuery = result
|
||||
}
|
||||
|
||||
function doneStateClick (id: Ref<DoneState>): void {
|
||||
@ -108,17 +107,17 @@
|
||||
selectedDS = ['NoDoneState']
|
||||
withoutDone = true
|
||||
}
|
||||
updateQuery(search, selectedDoneStates)
|
||||
updateQuery(query, selectedDoneStates)
|
||||
}
|
||||
|
||||
function noDoneClick (): void {
|
||||
withoutDone = true
|
||||
selectedDS = ['NoDoneState']
|
||||
selectedDoneStates.clear()
|
||||
updateQuery(search, selectedDoneStates)
|
||||
updateQuery(query, selectedDoneStates)
|
||||
}
|
||||
|
||||
$: updateQuery(search, selectedDoneStates)
|
||||
$: updateQuery(query, selectedDoneStates)
|
||||
const handleSelect = (result: any) => {
|
||||
if (result.type === 'select') {
|
||||
const res = result.detail
|
||||
@ -127,12 +126,12 @@
|
||||
state = undefined
|
||||
withoutDone = false
|
||||
selectedDoneStates.clear()
|
||||
updateQuery(search, selectedDoneStates)
|
||||
updateQuery(query, selectedDoneStates)
|
||||
} else if (res.id === 'DoneStates') {
|
||||
doneStatusesView = true
|
||||
state = undefined
|
||||
selectedDoneStates.clear()
|
||||
updateQuery(search, selectedDoneStates)
|
||||
updateQuery(query, selectedDoneStates)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,11 +157,11 @@
|
||||
{#if doneStatusesView}
|
||||
<TabList items={itemsDS} bind:selected={selectedDS} multiselect on:select={handleDoneSelect} size={'small'} />
|
||||
{:else}
|
||||
<StatesBar bind:state {space} gap={'none'} on:change={() => updateQuery(search, selectedDoneStates)} />
|
||||
<StatesBar bind:state {space} gap={'none'} on:change={() => updateQuery(query, selectedDoneStates)} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="statustableview-container">
|
||||
<TableBrowser {_class} bind:query config={resConfig} {options} showNotification />
|
||||
<TableBrowser {_class} bind:query={resultQuery} config={resConfig} {options} showNotification />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
@ -23,7 +23,6 @@
|
||||
import { getEventPositionElement, showPopup } from '@hcengineering/ui'
|
||||
import {
|
||||
ActionContext,
|
||||
FilterBar,
|
||||
focusStore,
|
||||
ListSelectionProvider,
|
||||
SelectDirection,
|
||||
@ -35,8 +34,7 @@
|
||||
|
||||
export let _class: Ref<Class<Task>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
// export let open: AnyComponent
|
||||
export let search: string
|
||||
export let query: DocumentQuery<Task>
|
||||
export let options: FindOptions<Task> | undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
// export let config: string[]
|
||||
@ -96,7 +94,8 @@
|
||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||
}
|
||||
const onContextMenu = (evt: any) => showMenu(evt.detail.evt, evt.detail.objects)
|
||||
let resultQuery = { doneState: null, space }
|
||||
|
||||
$: resultQuery = { ...query, doneState: null }
|
||||
</script>
|
||||
|
||||
{#await cardPresenter then presenter}
|
||||
@ -105,22 +104,13 @@
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
<FilterBar
|
||||
{_class}
|
||||
query={{ doneState: null, space }}
|
||||
on:change={(e) => {
|
||||
resultQuery = e.detail
|
||||
}}
|
||||
/>
|
||||
<KanbanUI
|
||||
bind:this={kanbanUI}
|
||||
{_class}
|
||||
{search}
|
||||
{options}
|
||||
query={resultQuery}
|
||||
{states}
|
||||
fieldName={'state'}
|
||||
rankFieldName={'rank'}
|
||||
on:content={onContent}
|
||||
on:obj-focus={onObjFocus}
|
||||
checked={$selectionStore ?? []}
|
||||
|
@ -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}
|
@ -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}
|
@ -39,6 +39,8 @@ import TodoItemsPopup from './components/todos/TodoItemsPopup.svelte'
|
||||
import Todos from './components/todos/Todos.svelte'
|
||||
import TodoStatePresenter from './components/todos/TodoStatePresenter.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'
|
||||
|
||||
@ -70,6 +72,8 @@ export default async (): Promise<Resources> => ({
|
||||
KanbanTemplateEditor,
|
||||
KanbanTemplateSelector,
|
||||
AssignedTasks,
|
||||
DoneStateRefPresenter,
|
||||
StateRefPresenter,
|
||||
TodoItemsPopup
|
||||
},
|
||||
actionImpl: {
|
||||
|
@ -35,4 +35,4 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
<IssuesView {query} title={tracker.string.ActiveIssues} />
|
||||
<IssuesView {query} space={currentSpace} title={tracker.string.ActiveIssues} />
|
||||
|
@ -15,17 +15,16 @@
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
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 { 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'
|
||||
|
||||
export let value: Employee | null | undefined
|
||||
export let issueId: Ref<Issue>
|
||||
export let issueClass: Ref<Class<Issue | IssueTemplate>> = tracker.class.Issue
|
||||
export let object: Issue | IssueTemplate
|
||||
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
@ -52,15 +51,9 @@
|
||||
return
|
||||
}
|
||||
|
||||
const currentIssue = await client.findOne(issueClass, { _id: issueId })
|
||||
|
||||
if (currentIssue === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const newAssignee = result === null ? null : result._id
|
||||
|
||||
await client.update(currentIssue, { assignee: newAssignee })
|
||||
await client.update(object, { assignee: newAssignee })
|
||||
}
|
||||
|
||||
const handleAssigneeEditorOpened = async (event: MouseEvent) => {
|
||||
|
@ -32,4 +32,4 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
<IssuesView {query} title={tracker.string.BacklogIssues} />
|
||||
<IssuesView {query} space={currentSpace} title={tracker.string.BacklogIssues} />
|
||||
|
@ -23,4 +23,4 @@
|
||||
$: query = { space: currentSpace }
|
||||
</script>
|
||||
|
||||
<IssuesView {query} title={tracker.string.AllIssues} />
|
||||
<IssuesView {query} space={currentSpace} title={tracker.string.AllIssues} />
|
||||
|
@ -1,23 +1,32 @@
|
||||
<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 { Viewlet } from '@hcengineering/view'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { viewOptionsStore } from '@hcengineering/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
|
||||
export let viewlet: WithLookup<Viewlet>
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let space: Ref<Space> | undefined
|
||||
|
||||
const createItemDialog = CreateIssue
|
||||
const createItemLabel = tracker.string.AddIssueTooltip
|
||||
</script>
|
||||
|
||||
{#if viewlet?.$lookup?.descriptor?.component}
|
||||
<Component
|
||||
is={viewlet.$lookup.descriptor.component}
|
||||
props={{
|
||||
_class: tracker.class.Issue,
|
||||
config: viewlet.config,
|
||||
options: viewlet.options,
|
||||
createItemDialog,
|
||||
createItemLabel,
|
||||
viewlet,
|
||||
query,
|
||||
viewOptions: $viewOptionsStore
|
||||
viewOptions: viewlet.viewOptions?.other,
|
||||
space,
|
||||
query
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -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>
|
@ -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)
|
||||
}}
|
||||
/>
|
@ -1,21 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { DocumentQuery, WithLookup } from '@hcengineering/core'
|
||||
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
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 { 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 IssuesHeader from './IssuesHeader.svelte'
|
||||
import { getDefaultViewOptionsConfig } from '../../utils'
|
||||
import tracker from '../../plugin'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let title: IntlString | undefined = undefined
|
||||
export let label: string = ''
|
||||
export let viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsConfig()
|
||||
export let space: Ref<Space> | undefined
|
||||
|
||||
export let panelWidth: number = 0
|
||||
|
||||
@ -64,12 +63,6 @@
|
||||
let docSize: boolean = false
|
||||
$: if (docWidth <= 900 && !docSize) docSize = true
|
||||
$: if (docWidth > 900 && docSize) docSize = false
|
||||
|
||||
onDestroy(
|
||||
location.subscribe(() => {
|
||||
viewOptionsConfig = viewOptionsConfig
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
|
||||
@ -78,7 +71,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="extra">
|
||||
{#if viewlet}
|
||||
<ViewOptionsButton viewOptionsKey={viewlet._id} config={viewOptionsConfig} />
|
||||
<ViewletSettingButton {viewlet} />
|
||||
{/if}
|
||||
{#if asideFloat && $$slots.aside}
|
||||
<div class="buttons-divider" />
|
||||
@ -98,7 +91,7 @@
|
||||
<FilterBar _class={tracker.class.Issue} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
|
||||
<div class="flex w-full h-full clear-mins">
|
||||
{#if viewlet}
|
||||
<IssuesContent {viewlet} query={resultQuery} />
|
||||
<IssuesContent {viewlet} query={resultQuery} {space} />
|
||||
{/if}
|
||||
{#if $$slots.aside !== undefined && asideShown}
|
||||
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
|
||||
|
@ -17,7 +17,8 @@
|
||||
import { Class, Doc, DocumentQuery, Lookup, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { Kanban, TypeState } from '@hcengineering/kanban'
|
||||
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 { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import {
|
||||
@ -30,18 +31,20 @@
|
||||
showPopup,
|
||||
tooltip
|
||||
} 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 Menu from '@hcengineering/view-resources/src/components/Menu.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import {
|
||||
getIssueStatusStates,
|
||||
getKanbanStatuses,
|
||||
getPriorityStates,
|
||||
issuesGroupBySorting,
|
||||
issuesSortOrderMap
|
||||
} from '../../utils'
|
||||
import { getIssueStatusStates, getKanbanStatuses, getPriorityStates, issuesGroupBySorting } from '../../utils'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
import ProjectEditor from '../projects/ProjectEditor.svelte'
|
||||
import AssigneePresenter from './AssigneePresenter.svelte'
|
||||
@ -53,24 +56,16 @@
|
||||
import StatusEditor from './StatusEditor.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 query: DocumentQuery<Issue> = {}
|
||||
export let viewOptions: {
|
||||
groupBy: IssuesGrouping
|
||||
orderBy: IssuesOrdering
|
||||
shouldShowEmptyGroups: boolean
|
||||
shouldShowSubIssues: boolean
|
||||
}
|
||||
export let viewOptions: ViewOptionModel[] | undefined
|
||||
|
||||
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
|
||||
$: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions)
|
||||
$: sort = { [orderBy]: issuesSortOrderMap[orderBy] }
|
||||
$: rankFieldName = orderBy === IssuesOrdering.Manual ? orderBy : undefined
|
||||
$: resultQuery = {
|
||||
...(shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }),
|
||||
...query
|
||||
} as any
|
||||
$: currentSpace = space || tracker.team.DefaultTeam
|
||||
$: groupBy = ($viewOptionsStore.groupBy ?? noCategory) as IssuesGrouping
|
||||
$: orderBy = $viewOptionsStore.orderBy
|
||||
$: sort = { [orderBy[0]]: orderBy[1] }
|
||||
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
const statusesQuery = createQuery()
|
||||
@ -80,6 +75,28 @@
|
||||
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
|
||||
$: issueStatusStates = getIssueStatusStates(issueStatuses)
|
||||
$: statusesQuery.query(
|
||||
@ -122,6 +139,12 @@
|
||||
}
|
||||
const issuesQuery = createQuery()
|
||||
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(
|
||||
tracker.class.Issue,
|
||||
resultQuery,
|
||||
@ -129,12 +152,7 @@
|
||||
issueStates = await getKanbanStatuses(groupBy, result)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
|
||||
project: tracker.class.Project,
|
||||
sprint: tracker.class.Sprint,
|
||||
assignee: contact.class.Employee
|
||||
},
|
||||
lookup: lookupIssue,
|
||||
sort: issuesGroupBySorting[groupBy]
|
||||
}
|
||||
)
|
||||
@ -145,17 +163,16 @@
|
||||
})
|
||||
function getIssueStates (
|
||||
groupBy: IssuesGrouping,
|
||||
showEmptyGroups: boolean,
|
||||
states: TypeState[],
|
||||
statusStates: 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.Priority) return priorityStates
|
||||
return []
|
||||
}
|
||||
$: states = getIssueStates(groupBy, shouldShowEmptyGroups, issueStates, issueStatusStates, priorityStates)
|
||||
$: states = getIssueStates(groupBy, issueStates, issueStatusStates, priorityStates)
|
||||
|
||||
const fullFilled: { [key: string]: boolean } = {}
|
||||
const getState = (state: any): WithLookup<IssueStatus> | undefined => {
|
||||
@ -174,12 +191,11 @@
|
||||
<Kanban
|
||||
bind:this={kanbanUI}
|
||||
_class={tracker.class.Issue}
|
||||
search=""
|
||||
{states}
|
||||
{dontUpdateRank}
|
||||
options={{ sort, lookup }}
|
||||
query={resultQuery}
|
||||
fieldName={groupBy}
|
||||
{rankFieldName}
|
||||
on:content={(evt) => {
|
||||
listProvider.update(evt.detail)
|
||||
}}
|
||||
@ -202,18 +218,16 @@
|
||||
<span class="lines-limit-2 ml-2">{state.title}</span>
|
||||
<span class="counter ml-2 text-md">{count}</span>
|
||||
</div>
|
||||
{#if groupBy === IssuesGrouping.Status}
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
|
||||
on:click={() => {
|
||||
showPopup(CreateIssue, { space: currentSpace, status: state._id }, 'top')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
|
||||
on:click={() => {
|
||||
showPopup(CreateIssue, { space: currentSpace, [groupBy]: state._id }, 'top')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
@ -244,7 +258,7 @@
|
||||
<AssigneePresenter
|
||||
value={issue.$lookup?.assignee}
|
||||
defaultClass={contact.class.Employee}
|
||||
issueId={issue._id}
|
||||
object={issue}
|
||||
isEditable={true}
|
||||
/>
|
||||
<div class="flex-center mt-2">
|
||||
@ -252,8 +266,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons-group xsmall-gap states-bar">
|
||||
{#if issue && issueStatuses && issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses} />
|
||||
{#if issue && issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentTeam} />
|
||||
{/if}
|
||||
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
|
||||
<ProjectEditor
|
||||
|
@ -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>
|
@ -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>
|
@ -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} />
|
@ -20,7 +20,7 @@
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
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 {
|
||||
Button,
|
||||
EditBox,
|
||||
@ -33,14 +33,7 @@
|
||||
showPopup,
|
||||
Spinner
|
||||
} from '@hcengineering/ui'
|
||||
import {
|
||||
ContextMenu,
|
||||
focusStore,
|
||||
ListSelectionProvider,
|
||||
SelectDirection,
|
||||
UpDownNavigator,
|
||||
viewOptionsStore
|
||||
} from '@hcengineering/view-resources'
|
||||
import { ContextMenu, UpDownNavigator } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import { generateIssueShortLink, getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
@ -49,8 +42,6 @@
|
||||
import CopyToClipboard from './CopyToClipboard.svelte'
|
||||
import SubIssues from './SubIssues.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 _class: Ref<Class<Issue>>
|
||||
@ -71,14 +62,6 @@
|
||||
let isEditing = false
|
||||
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())
|
||||
|
||||
$: read(_id)
|
||||
@ -126,72 +109,6 @@
|
||||
$: isDescriptionEmpty = !new DOMParser().parseFromString(description, 'text/html').documentElement.innerText?.trim()
|
||||
$: 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) {
|
||||
ev.preventDefault()
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
import { flip } from 'svelte/animate'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
import Circles from '../../icons/Circles.svelte'
|
||||
import AssigneeEditor from '../AssigneeEditor.svelte'
|
||||
import DueDateEditor from '../DueDateEditor.svelte'
|
||||
@ -41,6 +42,7 @@
|
||||
|
||||
function openIssue (target: Issue) {
|
||||
dispatch('issue-focus', target)
|
||||
subIssueListProvider(issues, target._id)
|
||||
showPanel(tracker.component.EditIssue, target._id, target._class, 'content')
|
||||
}
|
||||
|
||||
@ -120,7 +122,7 @@
|
||||
{issue.title}
|
||||
</span>
|
||||
{#if issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses.get(issue.space)} />
|
||||
<SubIssuesSelector value={issue} {currentTeam} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-center flex-no-shrink">
|
||||
|
@ -31,6 +31,7 @@
|
||||
import tracker from '../../../plugin'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import IssueStatusIcon from '../IssueStatusIcon.svelte'
|
||||
import { ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
|
||||
export let issue: WithLookup<Issue>
|
||||
|
||||
@ -48,6 +49,7 @@
|
||||
function openParentIssue () {
|
||||
if (parentIssue) {
|
||||
closeTooltip()
|
||||
ListSelectionProvider.Pop()
|
||||
openIssue(parentIssue._id)
|
||||
}
|
||||
}
|
||||
@ -138,7 +140,7 @@
|
||||
bind:this={subIssuesElement}
|
||||
class="flex-center sub-issues cursor-pointer"
|
||||
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>
|
||||
<div class="ml-2">
|
||||
|
@ -13,17 +13,26 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 { ButtonKind, ButtonSize, getPlatformColor } from '@hcengineering/ui'
|
||||
import { Button, closeTooltip, ProgressCircle, SelectPopup, showPanel, showPopup } from '@hcengineering/ui'
|
||||
import { updateFocus } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import {
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
closeTooltip,
|
||||
getPlatformColor,
|
||||
ProgressCircle,
|
||||
SelectPopup,
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
export let currentTeam: Team | undefined
|
||||
export let statuses: WithLookup<IssueStatus>[] | undefined
|
||||
|
||||
export let kind: ButtonKind = 'link-bordered'
|
||||
export let size: ButtonSize = 'inline'
|
||||
@ -32,21 +41,35 @@
|
||||
|
||||
let btn: HTMLElement
|
||||
|
||||
let subIssues: Issue[] | undefined
|
||||
let doneStatus: Ref<Doc> | undefined
|
||||
let subIssues: Issue[] = []
|
||||
let countComplate: number = 0
|
||||
|
||||
const query = createQuery()
|
||||
const statusesQuery = createQuery()
|
||||
|
||||
let statuses: WithLookup<IssueStatus>[] = []
|
||||
|
||||
$: if (value.$lookup?.subIssues !== undefined) {
|
||||
query.unsubscribe()
|
||||
subIssues = value.$lookup.subIssues as Issue[]
|
||||
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) {
|
||||
doneStatus = statuses.find((s) => s.category === tracker.issueStatusCategory.Completed)?._id ?? undefined
|
||||
if (doneStatus) countComplate = subIssues.filter((si) => si.status === doneStatus).length
|
||||
const doneStatuses = statuses.filter((s) => s.category === tracker.issueStatusCategory.Completed).map((p) => p._id)
|
||||
countComplate = subIssues.filter((si) => doneStatuses.includes(si.status)).length
|
||||
}
|
||||
$: 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 category = status?.$lookup?.category
|
||||
const color = status?.color ?? category?.color
|
||||
@ -59,9 +82,11 @@
|
||||
|
||||
function openIssue (target: Ref<Issue>) {
|
||||
if (target !== value._id) {
|
||||
subIssueListProvider(subIssues, target)
|
||||
showPanel(tracker.component.EditIssue, target, value._class, 'content')
|
||||
}
|
||||
}
|
||||
|
||||
function showSubIssues () {
|
||||
if (subIssues) {
|
||||
closeTooltip()
|
||||
@ -71,7 +96,7 @@
|
||||
value: subIssues.map((iss) => {
|
||||
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'
|
||||
},
|
||||
@ -86,12 +111,6 @@
|
||||
},
|
||||
(selectedIssue) => {
|
||||
selectedIssue !== undefined && openIssue(selectedIssue)
|
||||
},
|
||||
(selectedIssue) => {
|
||||
const focus = subIssues?.find((it) => it._id === selectedIssue.id)
|
||||
if (focus !== undefined) {
|
||||
updateFocus({ focus })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -70,7 +70,7 @@
|
||||
config={[
|
||||
'$lookup.attachedTo',
|
||||
'',
|
||||
'$lookup.employee',
|
||||
'employee',
|
||||
{
|
||||
key: '$lookup.attachedTo',
|
||||
presenter: ParentNamesPresenter,
|
||||
|
@ -19,10 +19,8 @@
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import type { Issue } from '@hcengineering/tracker'
|
||||
import { ViewOptionModel } from '@hcengineering/view-resources'
|
||||
|
||||
import tracker from '../../plugin'
|
||||
import { getDefaultViewOptionsConfig } from '../../utils'
|
||||
import IssuesView from '../issues/IssuesView.svelte'
|
||||
import ModeSelector from '../ModeSelector.svelte'
|
||||
|
||||
@ -36,8 +34,6 @@
|
||||
let created = { _id: { $in: [] as Ref<Issue>[] } }
|
||||
let subscribed = { _id: { $in: [] as Ref<Issue>[] } }
|
||||
|
||||
const viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsConfig(true)
|
||||
|
||||
const createdQuery = createQuery()
|
||||
$: createdQuery.query<TxCollectionCUD<Issue, Issue>>(
|
||||
core.class.TxCollectionCUD,
|
||||
@ -79,7 +75,7 @@
|
||||
$: query = getQuery(mode, { assigned, created, subscribed })
|
||||
</script>
|
||||
|
||||
<IssuesView {query} title={tracker.string.MyIssues} {viewOptionsConfig}>
|
||||
<IssuesView {query} space={undefined} title={tracker.string.MyIssues}>
|
||||
<svelte:fragment slot="afterHeader">
|
||||
<ModeSelector {config} {mode} onChange={handleChangeMode} />
|
||||
</svelte:fragment>
|
||||
|
@ -34,7 +34,7 @@
|
||||
})
|
||||
</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">
|
||||
<Button size={'small'} kind={'link'} on:click={selectProject}>
|
||||
<svelte:fragment slot="content">
|
||||
|
@ -44,7 +44,7 @@
|
||||
})
|
||||
</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">
|
||||
<div bind:this={container}>
|
||||
<Button size={'small'} kind={'link'} on:click={selectSprint}>
|
||||
|
@ -20,13 +20,13 @@
|
||||
import tracker from '../../plugin'
|
||||
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.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 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(
|
||||
(it) => issueStatuses.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
|
||||
@ -86,12 +86,12 @@
|
||||
})
|
||||
.reduce((it, cur) => {
|
||||
return it + cur
|
||||
}),
|
||||
}, 0),
|
||||
3
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if issues}
|
||||
{#if docs}
|
||||
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
|
||||
<div class="flex-row-center flex-no-shrink h-6" class:showWarning={totalEstimation > (capacity ?? 0)}>
|
||||
<EstimationProgressCircle value={totalReported} max={totalEstimation} />
|
||||
|
@ -17,8 +17,13 @@
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueTemplate, Sprint, Team } from '@hcengineering/tracker'
|
||||
import { ButtonKind, ButtonShape, ButtonSize, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
|
||||
import {
|
||||
ButtonKind,
|
||||
ButtonShape,
|
||||
ButtonSize,
|
||||
DatePresenter,
|
||||
deviceOptionsStore as deviceInfo
|
||||
} from '@hcengineering/ui'
|
||||
import { activeSprint } from '../../issues'
|
||||
import tracker from '../../plugin'
|
||||
import { getDayOfSprint } from '../../utils'
|
||||
|
@ -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>
|
@ -1,17 +1,30 @@
|
||||
<script lang="ts">
|
||||
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 { viewOptionsStore } from '@hcengineering/view-resources'
|
||||
import TemplatesList from './TemplatesList.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import CreateIssueTemplate from './CreateIssueTemplate.svelte'
|
||||
|
||||
export let viewlet: WithLookup<Viewlet>
|
||||
export let query: DocumentQuery<IssueTemplate> = {}
|
||||
|
||||
$: vo = $viewOptionsStore as ViewOptions
|
||||
const createItemDialog = CreateIssueTemplate
|
||||
const createItemLabel = tracker.string.IssueTemplate
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
|
@ -4,8 +4,9 @@
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { IssueTemplate } from '@hcengineering/tracker'
|
||||
import { Button, IconAdd, IconDetails, IconDetailsFilled, showPopup } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { FilterBar, getActiveViewletId, ViewOptionModel, ViewOptionsButton } from '@hcengineering/view-resources'
|
||||
import view, { Viewlet, ViewOptionModel } from '@hcengineering/view'
|
||||
import { FilterBar, getActiveViewletId } from '@hcengineering/view-resources'
|
||||
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { getDefaultViewOptionsTemplatesConfig } from '../../utils'
|
||||
import IssuesHeader from '../issues/IssuesHeader.svelte'
|
||||
@ -85,7 +86,7 @@
|
||||
/>
|
||||
|
||||
{#if viewlet}
|
||||
<ViewOptionsButton viewOptionsKey={viewlet._id} config={viewOptionsConfig} />
|
||||
<ViewletSettingButton {viewlet} />
|
||||
{/if}
|
||||
|
||||
{#if asideFloat && $$slots.aside}
|
||||
|
@ -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>
|
@ -32,8 +32,8 @@ import IssuePreview from './components/issues/IssuePreview.svelte'
|
||||
import Issues from './components/issues/Issues.svelte'
|
||||
import IssuesView from './components/issues/IssuesView.svelte'
|
||||
import KanbanView from './components/issues/KanbanView.svelte'
|
||||
import ListView from './components/issues/ListView.svelte'
|
||||
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
|
||||
import PriorityRefPresenter from './components/issues/PriorityRefPresenter.svelte'
|
||||
import PriorityEditor from './components/issues/PriorityEditor.svelte'
|
||||
import PriorityPresenter from './components/issues/PriorityPresenter.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 SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte'
|
||||
import GrowPresenter from './components/issues/GrowPresenter.svelte'
|
||||
import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte'
|
||||
import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.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 TemplateEstimationEditor from './components/templates/EstimationEditor.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 CreateTeam from './components/teams/CreateTeam.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> (
|
||||
_class: Ref<Class<D>>,
|
||||
@ -229,6 +231,8 @@ export default async (): Promise<Resources> => ({
|
||||
ModificationDatePresenter,
|
||||
PriorityPresenter,
|
||||
PriorityEditor,
|
||||
PriorityRefPresenter,
|
||||
SprintRefPresenter,
|
||||
ProjectEditor,
|
||||
StatusPresenter,
|
||||
StatusEditor,
|
||||
@ -246,7 +250,6 @@ export default async (): Promise<Resources> => ({
|
||||
SetParentIssueActionPopup,
|
||||
EditProject,
|
||||
IssuesView,
|
||||
ListView,
|
||||
KanbanView,
|
||||
TeamProjects,
|
||||
Roadmap,
|
||||
@ -265,7 +268,6 @@ export default async (): Promise<Resources> => ({
|
||||
TimeSpendReport,
|
||||
EstimationEditor,
|
||||
SubIssuesSelector,
|
||||
GrowPresenter,
|
||||
RelatedIssues,
|
||||
RelatedIssueTemplates,
|
||||
ProjectSelector,
|
||||
@ -274,7 +276,9 @@ export default async (): Promise<Resources> => ({
|
||||
EditIssueTemplate,
|
||||
TemplateEstimationEditor,
|
||||
CreateTeam,
|
||||
TeamPresenter
|
||||
TeamPresenter,
|
||||
IssueStatistics,
|
||||
StatusRefPresenter
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||
@ -284,7 +288,11 @@ export default async (): Promise<Resources> => ({
|
||||
IssueTitleProvider: getIssueTitle,
|
||||
GetIssueId: issueIdProvider,
|
||||
GetIssueLink: issueLinkProvider,
|
||||
GetIssueTitle: issueTitleProvider
|
||||
GetIssueTitle: issueTitleProvider,
|
||||
IssueStatusSort: issueStatusSort,
|
||||
IssuePrioritySort: issuePrioritySort,
|
||||
SprintSort: sprintSort,
|
||||
SubIssueQuery: subIssueQuery
|
||||
},
|
||||
actionImpl: {
|
||||
EditWorkflowStatuses: editWorkflowStatuses,
|
||||
|
@ -12,12 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// 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 { mergeIds } from '@hcengineering/platform'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import tracker, { trackerId } from '../../tracker/lib'
|
||||
import { IssueDraft } from '@hcengineering/tracker'
|
||||
import { SortFunc } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(trackerId, tracker, {
|
||||
string: {
|
||||
@ -292,9 +293,11 @@ export default mergeIds(trackerId, tracker, {
|
||||
ModificationDatePresenter: '' as AnyComponent,
|
||||
PriorityPresenter: '' as AnyComponent,
|
||||
PriorityEditor: '' as AnyComponent,
|
||||
PriorityRefPresenter: '' as AnyComponent,
|
||||
ProjectEditor: '' as AnyComponent,
|
||||
SprintEditor: '' as AnyComponent,
|
||||
StatusPresenter: '' as AnyComponent,
|
||||
StatusRefPresenter: '' as AnyComponent,
|
||||
StatusEditor: '' as AnyComponent,
|
||||
AssigneePresenter: '' as AnyComponent,
|
||||
DueDatePresenter: '' as AnyComponent,
|
||||
@ -312,13 +315,12 @@ export default mergeIds(trackerId, tracker, {
|
||||
SetParentIssueActionPopup: '' as AnyComponent,
|
||||
EditProject: '' as AnyComponent,
|
||||
IssuesView: '' as AnyComponent,
|
||||
ListView: '' as AnyComponent,
|
||||
KanbanView: '' as AnyComponent,
|
||||
Roadmap: '' as AnyComponent,
|
||||
TeamProjects: '' as AnyComponent,
|
||||
IssuePreview: '' as AnyComponent,
|
||||
RelationsPopup: '' as AnyComponent,
|
||||
|
||||
SprintRefPresenter: '' as AnyComponent,
|
||||
Sprints: '' as AnyComponent,
|
||||
SprintPresenter: '' as AnyComponent,
|
||||
SprintStatusPresenter: '' as AnyComponent,
|
||||
@ -328,7 +330,6 @@ export default mergeIds(trackerId, tracker, {
|
||||
TimeSpendReport: '' as AnyComponent,
|
||||
EstimationEditor: '' as AnyComponent,
|
||||
TemplateEstimationEditor: '' as AnyComponent,
|
||||
GrowPresenter: '' as AnyComponent,
|
||||
|
||||
ProjectSelector: '' as AnyComponent,
|
||||
|
||||
@ -342,6 +343,10 @@ export default mergeIds(trackerId, tracker, {
|
||||
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
|
||||
GetIssueId: '' 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>>
|
||||
}
|
||||
})
|
||||
|
@ -102,5 +102,5 @@ export const issuesGroupBySorting: Record<IssuesGrouping, SortingQuery<Issue>> =
|
||||
[IssuesGrouping.Priority]: { priority: SortingOrder.Ascending },
|
||||
[IssuesGrouping.Project]: { '$lookup.project.label': SortingOrder.Ascending },
|
||||
[IssuesGrouping.Sprint]: { '$lookup.sprint.label': SortingOrder.Ascending },
|
||||
[IssuesGrouping.NoGrouping]: {}
|
||||
[IssuesGrouping.NoGrouping]: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
|
@ -13,24 +13,23 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Employee, formatName } from '@hcengineering/contact'
|
||||
import { DocumentQuery, Ref, SortingOrder, TxOperations, WithLookup } from '@hcengineering/core'
|
||||
import { Doc, DocumentQuery, Ref, SortingOrder, TxOperations, WithLookup } from '@hcengineering/core'
|
||||
import { TypeState } from '@hcengineering/kanban'
|
||||
import { Asset, IntlString, translate } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import {
|
||||
Issue,
|
||||
IssuePriority,
|
||||
IssuesDateModificationPeriod,
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
IssueStatus,
|
||||
IssueTemplate,
|
||||
ProjectStatus,
|
||||
Sprint,
|
||||
SprintStatus,
|
||||
Team,
|
||||
TimeReportDayType
|
||||
} from '@hcengineering/tracker'
|
||||
import { ViewOptionModel } from '@hcengineering/view-resources'
|
||||
import {
|
||||
AnyComponent,
|
||||
AnySvelteComponent,
|
||||
@ -39,6 +38,8 @@ import {
|
||||
isWeekend,
|
||||
MILLISECONDS_IN_WEEK
|
||||
} from '@hcengineering/ui'
|
||||
import { ViewOptionModel } from '@hcengineering/view'
|
||||
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import tracker from './plugin'
|
||||
import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types'
|
||||
|
||||
@ -306,92 +307,36 @@ const listIssueStatusOrder = [
|
||||
tracker.issueStatusCategory.Canceled
|
||||
] as const
|
||||
|
||||
export function getCategories (
|
||||
key: IssuesGroupByKeys | undefined,
|
||||
elements: Array<WithLookup<Issue | IssueTemplate>>,
|
||||
shouldShowAll: boolean,
|
||||
statuses: IssueStatus[],
|
||||
employees: Employee[]
|
||||
): any[] {
|
||||
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') {
|
||||
return defaultPriorities
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'status') {
|
||||
existingCategories.sort((s1, s2) => {
|
||||
const i1 = defaultStatuses.findIndex((x) => x === s1)
|
||||
const i2 = defaultStatuses.findIndex((x) => x === s2)
|
||||
|
||||
return i1 - i2
|
||||
export async function issueStatusSort (value: Array<Ref<IssueStatus>>): Promise<Array<Ref<IssueStatus>>> {
|
||||
return await new Promise((resolve) => {
|
||||
const query = createQuery(true)
|
||||
query.query(tracker.class.IssueStatus, { _id: { $in: value } }, (res) => {
|
||||
res.sort((a, b) => listIssueStatusOrder.indexOf(a.category) - listIssueStatusOrder.indexOf(b.category))
|
||||
resolve(res.map((p) => p._id))
|
||||
query.unsubscribe()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (key === 'priority') {
|
||||
existingCategories.sort((p1, p2) => {
|
||||
const i1 = defaultPriorities.findIndex((x) => x === p1)
|
||||
const i2 = defaultPriorities.findIndex((x) => x === p2)
|
||||
export async function issuePrioritySort (value: IssuePriority[]): Promise<IssuePriority[]> {
|
||||
value.sort((a, b) => {
|
||||
const i1 = defaultPriorities.indexOf(a)
|
||||
const i2 = defaultPriorities.indexOf(b)
|
||||
|
||||
return i1 - i2
|
||||
return i1 - i2
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
export async function sprintSort (value: Array<Ref<Sprint>>): Promise<Array<Ref<Sprint>>> {
|
||||
return await new Promise((resolve) => {
|
||||
const query = createQuery(true)
|
||||
query.query(tracker.class.Sprint, { _id: { $in: value } }, (res) => {
|
||||
res.sort((a, b) => (b?.startDate ?? 0) - (a?.startDate ?? 0))
|
||||
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 (
|
||||
@ -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[] {
|
||||
const groupByCategory: ViewOptionModel = {
|
||||
key: 'groupBy',
|
||||
@ -565,7 +461,7 @@ export function getDefaultViewOptionsTemplatesConfig (): ViewOptionModel[] {
|
||||
{ id: 'priority', label: tracker.string.Priority },
|
||||
{ id: 'project', label: tracker.string.Project },
|
||||
{ id: 'sprint', label: tracker.string.Sprint },
|
||||
{ id: 'noGrouping', label: tracker.string.NoGrouping }
|
||||
{ id: '#no_category', label: tracker.string.NoGrouping }
|
||||
],
|
||||
type: 'dropdown'
|
||||
}
|
||||
@ -670,3 +566,28 @@ export function getTimeReportDayType (timestamp: number): TimeReportDayType | un
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ export enum IssuesGrouping {
|
||||
Priority = 'priority',
|
||||
Project = 'project',
|
||||
Sprint = 'sprint',
|
||||
NoGrouping = 'noGrouping'
|
||||
NoGrouping = '#no_category'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,6 +49,12 @@
|
||||
"MatchCriteria": "Match criteria",
|
||||
"DontMatchCriteria": "Don't match criteria",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,12 @@
|
||||
"MatchCriteria": "Соответсвует условию",
|
||||
"DontMatchCriteria": "Не соответвует условию",
|
||||
"View": "Вид",
|
||||
"MarkupEditor": "Изменение форматированного поля"
|
||||
"MarkupEditor": "Изменение форматированного поля",
|
||||
"List": "Список",
|
||||
"Select": "Выбрать",
|
||||
"NoGrouping": "Нет группировки",
|
||||
"Grouping": "Группировка",
|
||||
"Ordering": "Сортировка",
|
||||
"Manual": "Пользовательский"
|
||||
}
|
||||
}
|
||||
|
@ -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} />
|
@ -21,18 +21,24 @@
|
||||
export let key: string
|
||||
export let justify: string = ''
|
||||
let prevKey = key
|
||||
let element: HTMLDivElement | undefined
|
||||
|
||||
let cWidth: number = 0
|
||||
|
||||
afterUpdate(() => {
|
||||
if (prevKey !== key) {
|
||||
$fixedWidthStore[prevKey] = 0
|
||||
$fixedWidthStore[key] = 0
|
||||
prevKey = key
|
||||
cWidth = 0
|
||||
}
|
||||
})
|
||||
|
||||
$: if (cWidth > ($fixedWidthStore[key] ?? 0)) {
|
||||
$fixedWidthStore[key] = cWidth
|
||||
function resize (element: Element) {
|
||||
cWidth = element.clientWidth
|
||||
if (cWidth > ($fixedWidthStore[key] ?? 0)) {
|
||||
$fixedWidthStore[key] = cWidth
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
@ -41,13 +47,10 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={element}
|
||||
class="flex-no-shrink"
|
||||
style="{justify !== '' ? `text-align: ${justify}; ` : ''} min-width: {$fixedWidthStore[key] ?? 0}px;"
|
||||
use:resizeObserver={(element) => {
|
||||
if (element.clientWidth > cWidth) {
|
||||
cWidth = element.clientWidth
|
||||
}
|
||||
}}
|
||||
use:resizeObserver={resize}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -41,7 +41,7 @@
|
||||
(value as Doc)?.space === undefined
|
||||
) {
|
||||
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) {
|
||||
docQuery.unsubscribe()
|
||||
|
@ -24,9 +24,6 @@
|
||||
export let options: FindOptions<Doc> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let config: string[]
|
||||
export let search: string = ''
|
||||
|
||||
$: resultQuery = search === '' ? { space, ...query } : { $search: search, space, ...query }
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@ -34,4 +31,4 @@
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
<TableBrowser {_class} {config} {options} query={resultQuery} {baseMenuClass} showNotification />
|
||||
<TableBrowser {_class} {config} {options} {query} {baseMenuClass} showNotification />
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import {
|
||||
import ui, {
|
||||
Button,
|
||||
IconNavPrev,
|
||||
IconDownOutline,
|
||||
@ -12,7 +12,6 @@
|
||||
import { tick } from 'svelte'
|
||||
import { select } from '../actionImpl'
|
||||
import { focusStore } from '../selection'
|
||||
import tracker from '../../../tracker-resources/src/plugin'
|
||||
|
||||
export let element: Doc
|
||||
|
||||
@ -41,7 +40,7 @@
|
||||
<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
|
||||
showTooltip={{ label: tracker.string.Back, direction: 'bottom' }}
|
||||
showTooltip={{ label: ui.string.Back, direction: 'bottom' }}
|
||||
icon={IconNavPrev}
|
||||
kind={'secondary'}
|
||||
size={'medium'}
|
||||
|
91
plugins/view-resources/src/components/ViewOptions.svelte
Normal file
91
plugins/view-resources/src/components/ViewOptions.svelte
Normal 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>
|
@ -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
Loading…
Reference in New Issue
Block a user