List extract (#2505)

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

View File

@ -77,7 +77,7 @@ export class TSavedAttachments extends TPreference implements SavedAttachments {
export function createModel (builder: Builder): void {
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
})

View File

@ -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
})

View File

@ -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
})
}

View File

@ -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
})

View File

@ -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]
})

View File

@ -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,

View File

@ -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
})

View File

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

View File

@ -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
)

View File

@ -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

View File

@ -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
})

View File

@ -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>

View File

@ -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
})

View File

@ -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
})

View File

@ -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
})

View File

@ -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
})

View File

@ -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
})

View File

@ -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>

View File

@ -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
})

View File

@ -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>

View File

@ -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,
{

View File

@ -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>>>,

View File

@ -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
})
}

View File

@ -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)

View File

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

View File

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

View File

@ -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>,

View File

@ -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'

View File

@ -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',

View File

@ -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)
}}

View File

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

View File

@ -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)) {

View File

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

View File

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

View File

@ -0,0 +1,33 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Contact } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import contact from '../plugin'
import ContactPresenter from './ContactPresenter.svelte'
export let value: Ref<Contact>
export let isInteractive = true
let doc: Contact | undefined
const query = createQuery()
$: value && query.query(contact.class.Contact, { _id: value }, (res) => ([doc] = res), { limit: 1 })
</script>
{#if doc}
<ContactPresenter value={doc} {isInteractive} />
{/if}

View File

@ -2,21 +2,14 @@
import { Employee } from '@hcengineering/contact'
import { 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}

View File

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

View File

@ -15,8 +15,10 @@
<script lang="ts">
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}

View File

@ -0,0 +1,59 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact, { Person } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { PersonLabelTooltip } from '..'
import PersonPresenter from './PersonPresenter.svelte'
export let value: Ref<Person> | null | undefined
export let inline = false
export let enlargedText = false
export let isInteractive = true
export let shouldShowAvatar = true
export let shouldShowName = true
export let shouldShowPlaceholder = false
export let defaultName: IntlString | undefined = undefined
export let tooltipLabels: PersonLabelTooltip | undefined = undefined
export let avatarSize: 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' = 'x-small'
export let onEdit: ((event: MouseEvent) => void) | undefined = undefined
let person: Person | undefined
const query = createQuery()
$: value && query.query(contact.class.Person, { _id: value }, (res) => ([person] = res), { limit: 1 })
function getValue (person: Person | undefined, value: Ref<Person> | null | undefined): Person | null | undefined {
if (value === undefined || value === null) {
return value
}
return person
}
</script>
<PersonPresenter
value={getValue(person, value)}
{onEdit}
{avatarSize}
{tooltipLabels}
{defaultName}
{shouldShowAvatar}
{shouldShowName}
{shouldShowPlaceholder}
{enlargedText}
{isInteractive}
{inline}
/>

View File

@ -17,9 +17,9 @@
import { Channel, Contact, Employee, formatName, getGravatarUrl } from '@hcengineering/contact'
import { 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
}
})

View File

@ -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
}
})

View File

@ -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()
})
})
}

View File

@ -0,0 +1,33 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import inventory, { Category } from '@hcengineering/inventory'
import { createQuery } from '@hcengineering/presentation'
import CategoryPresenter from './CategoryPresenter.svelte'
export let value: Ref<Category>
export let inline: boolean = false
let category: Category | undefined
const query = createQuery()
$: query.query(inventory.class.Category, { _id: value }, (res) => ([category] = res), { limit: 1 })
</script>
{#if category}
<CategoryPresenter {inline} value={category} />
{/if}

View File

@ -19,6 +19,7 @@ import { Resources } from '@hcengineering/platform'
import { showPopup } from '@hcengineering/ui'
import 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,

View File

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

View File

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

View File

@ -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}

View File

@ -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}
/>

View File

@ -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 }} />

View File

@ -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,

View File

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

View File

@ -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 }}
/>

View File

@ -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 }}

View File

@ -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
/>

View File

@ -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 }
}
: { space }
let resultQuery = {
space
_id: { $in: ids },
...query
}
: 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} />

View File

@ -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">

View File

@ -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 ?? []}

View File

@ -0,0 +1,33 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import type { DoneState } from '@hcengineering/task'
import task from '@hcengineering/task'
import DoneStatePresenter from './DoneStatePresenter.svelte'
export let value: Ref<DoneState>
export let showTitle: boolean = true
let state: DoneState | undefined
const query = createQuery()
$: query.query(task.class.DoneState, { _id: value }, (res) => ([state] = res), { limit: 1 })
</script>
{#if state}
<DoneStatePresenter value={state} {showTitle} />
{/if}

View File

@ -0,0 +1,31 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import task, { State } from '@hcengineering/task'
import StatePresenter from './StatePresenter.svelte'
export let value: Ref<State>
let state: State | undefined
const query = createQuery()
$: query.query(task.class.State, { _id: value }, (res) => ([state] = res), { limit: 1 })
</script>
{#if state}
<StatePresenter value={state} />
{/if}

View File

@ -39,6 +39,8 @@ import TodoItemsPopup from './components/todos/TodoItemsPopup.svelte'
import Todos from './components/todos/Todos.svelte'
import 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: {

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

@ -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}

View File

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

View File

@ -1,82 +0,0 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
import { BuildModelKey } from '@hcengineering/view'
import {
ActionContext,
focusStore,
ListSelectionProvider,
SelectDirection,
selectionStore,
LoadingProps
} from '@hcengineering/view-resources'
import IssuesList from './IssuesList.svelte'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Employee } from '@hcengineering/contact'
import { onMount } from 'svelte'
import { IssuesGroupByKeys, IssuesOrderByKeys } from '../../utils'
export let _class: Ref<Class<Doc>>
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let itemsConfig: (BuildModelKey | string)[]
export let currentSpace: Ref<Team> | undefined = undefined
export let groupByKey: IssuesGroupByKeys | undefined = undefined
export let orderBy: IssuesOrderByKeys
export let statuses: WithLookup<IssueStatus>[]
export let employees: (WithLookup<Employee> | undefined)[] = []
export let categories: any[] = []
export let groupedIssues: { [key: string | number | symbol]: Issue[] } = {}
export let loadingProps: LoadingProps | undefined = undefined
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
let issuesList: IssuesList
$: if (issuesList !== undefined) listProvider.update(Object.values(groupedIssues).flat(1))
onMount(() => {
;(document.activeElement as HTMLElement)?.blur()
})
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<IssuesList
bind:this={issuesList}
{_class}
{baseMenuClass}
{currentSpace}
{groupByKey}
{orderBy}
{statuses}
{employees}
{categories}
{itemsConfig}
{groupedIssues}
{loadingProps}
selectedObjectIds={$selectionStore ?? []}
selectedRowIndex={listProvider.current($focusStore)}
on:row-focus={(event) => {
listProvider.updateFocus(event.detail ?? undefined)
}}
on:check={(event) => {
listProvider.updateSelection(event.detail.docs, event.detail.value)
}}
/>

View File

@ -1,21 +1,20 @@
<script lang="ts">
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}>

View File

@ -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')
showPopup(CreateIssue, { space: currentSpace, [groupBy]: state._id }, 'top')
}}
/>
</div>
{/if}
</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

View File

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

View File

@ -0,0 +1,74 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { IssuePriority } from '@hcengineering/tracker'
import { Button, ButtonKind, ButtonSize, Icon, Label } from '@hcengineering/ui'
import { issuePriorities } from '../../utils'
export let value: IssuePriority
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
</script>
{#if kind === 'list' || kind === 'list-header'}
<div class="priority-container">
<div class="icon">
{#if issuePriorities[value]?.icon}<Icon icon={issuePriorities[value]?.icon} {size} />{/if}
</div>
<span
class="{kind === 'list' ? 'ml-2 text-md' : 'ml-3 text-base'} overflow-label disabled fs-bold content-accent-color"
>
<Label label={issuePriorities[value]?.label} />
</span>
</div>
{:else}
<Button
label={issuePriorities[value]?.label}
icon={issuePriorities[value]?.icon}
{justify}
{width}
{size}
{kind}
disabled
/>
{/if}
<style lang="scss">
.priority-container {
display: flex;
align-items: center;
flex-shrink: 0;
min-width: 0;
cursor: pointer;
.icon {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: 1rem;
height: 1rem;
color: var(--content-color);
}
&:hover {
.icon {
color: var(--caption-color) !important;
}
}
}
</style>

View File

@ -0,0 +1,32 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import tracker, { IssueStatus } from '@hcengineering/tracker'
import StatusPresenter from './StatusPresenter.svelte'
export let value: Ref<IssueStatus> | undefined
export let size: 'small' | 'medium' = 'medium'
let status: WithLookup<IssueStatus> | undefined
const query = createQuery()
$: query.query(tracker.class.IssueStatus, { _id: value }, (res) => ([status] = res), {
lookup: { category: tracker.class.IssueStatusCategory }
})
</script>
<StatusPresenter value={status} {size} />

View File

@ -20,7 +20,7 @@
import { getResource } from '@hcengineering/platform'
import 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()

View File

@ -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">

View File

@ -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">

View File

@ -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 })
}
}
)
}

View File

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

View File

@ -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>

View File

@ -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">

View File

@ -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}>

View File

@ -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} />

View File

@ -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'

View File

@ -0,0 +1,76 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Sprint, Team } from '@hcengineering/tracker'
import { ButtonKind, DatePresenter, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import tracker from '../../plugin'
import { getDayOfSprint } from '../../utils'
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
import SprintSelector from './SprintSelector.svelte'
export let value: Ref<Sprint>
export let kind: ButtonKind = 'link'
const spaceQuery = createQuery()
let currentTeam: Team | undefined
$: sprint &&
spaceQuery.query(tracker.class.Team, { _id: sprint.space }, (res) => {
;[currentTeam] = res
})
$: workDayLength = currentTeam?.workDayLength
const sprintQuery = createQuery()
let sprint: Sprint | undefined
$: sprintQuery.query(tracker.class.Sprint, { _id: value }, (res) => {
;[sprint] = res
})
$: twoRows = $deviceInfo.twoRows
</script>
<div
class="flex flex-wrap"
class:minus-margin={kind === 'list-header'}
style:flex-direction={twoRows ? 'column' : 'row'}
>
<div class="flex-row-center" class:minus-margin-vSpace={kind === 'list-header'}>
<SprintSelector {kind} isEditable={false} enlargedText {value} />
</div>
{#if sprint && kind === 'list-header'}
<div class="flex-row-center" class:minus-margin-space={kind === 'list-header'} class:text-sm={twoRows}>
{#if sprint}
{@const now = Date.now()}
{@const sprintDaysFrom =
now < sprint.startDate
? 0
: now > sprint.targetDate
? getDayOfSprint(sprint.startDate, sprint.targetDate)
: getDayOfSprint(sprint.startDate, now)}
{@const sprintDaysTo = getDayOfSprint(sprint.startDate, sprint.targetDate)}
<DatePresenter value={sprint.startDate} kind={'transparent'} />
<span class="p-1"> / </span>
<DatePresenter value={sprint.targetDate} kind={'transparent'} />
<div class="w-2 min-w-2" />
<!-- Active sprint in time -->
<TimePresenter value={sprintDaysFrom} {workDayLength} />
/
<TimePresenter value={sprintDaysTo} {workDayLength} />
{/if}
</div>
{/if}
</div>

View File

@ -1,17 +1,30 @@
<script lang="ts">
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}

View File

@ -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}

View File

@ -1,65 +0,0 @@
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { IssueTemplate, ViewOptions } from '@hcengineering/tracker'
import { defaultSP, Scroller } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import tracker from '../../plugin'
import {
getCategories,
groupBy as groupByFunc,
issuesGroupKeyMap,
issuesOrderKeyMap,
issuesSortOrderMap
} from '../../utils'
import IssuesListBrowser from '../issues/IssuesListBrowser.svelte'
export let _class: Ref<Class<Doc>>
export let config: (string | BuildModelKey)[]
export let query: DocumentQuery<IssueTemplate> = {}
export let viewOptions: ViewOptions
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
$: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions)
$: groupByKey = issuesGroupKeyMap[groupBy]
$: orderByKey = issuesOrderKeyMap[orderBy]
$: groupedIssues = groupByFunc(issues, groupBy)
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups, [], employees)
$: employees = issues.map((x) => x.$lookup?.assignee).filter(Boolean) as Employee[]
const issuesQuery = createQuery()
let issues: WithLookup<IssueTemplate>[] = []
$: issuesQuery.query(
tracker.class.IssueTemplate,
{ ...query },
(result) => {
issues = result
},
{
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
lookup: {
assignee: contact.class.Employee,
space: tracker.class.Team,
sprint: tracker.class.Sprint
}
}
)
</script>
<div class="w-full h-full clear-mins">
<Scroller fade={defaultSP}>
<IssuesListBrowser
{_class}
{currentSpace}
{groupByKey}
orderBy={orderByKey}
statuses={[]}
{employees}
{categories}
itemsConfig={config}
{groupedIssues}
/>
</Scroller>
</div>

View File

@ -32,8 +32,8 @@ import IssuePreview from './components/issues/IssuePreview.svelte'
import Issues from './components/issues/Issues.svelte'
import 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,

View File

@ -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>>
}
})

View File

@ -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 }
}

View File

@ -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()
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()
})
})
}
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)
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 value
}
if (key === 'priority') {
existingCategories.sort((p1, p2) => {
const i1 = defaultPriorities.findIndex((x) => x === p1)
const i2 = defaultPriorities.findIndex((x) => x === p2)
return i1 - i2
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)
}
}

View File

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

View File

@ -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"
}
}

View File

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

View File

@ -0,0 +1,26 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
export let value: Ref<Class<Doc>>
const client = getClient()
const _class = client.getModel().getObject(value)
</script>
<Label label={_class.label} />

View File

@ -21,19 +21,25 @@
export let key: string
export let 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)) {
function resize (element: Element) {
cWidth = element.clientWidth
if (cWidth > ($fixedWidthStore[key] ?? 0)) {
$fixedWidthStore[key] = cWidth
}
}
onDestroy(() => {
$fixedWidthStore[key] = 0
@ -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>

View File

@ -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()

View File

@ -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 />

View File

@ -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'}

View File

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

View File

@ -1,72 +0,0 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Button, eventToHTMLElement, IconDownOutline, showPopup, Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
import { getViewOptions, setViewOptions, viewOptionsStore, ViewOptionModel } from '../viewOptions'
export let config: ViewOptionModel[]
export let viewOptionsKey: string
$: loadViewOptionsStore(config, viewOptionsKey)
function loadViewOptionsStore (config: ViewOptionModel[], key: string) {
viewOptionsStore.set(
config.reduce(
(options, { key, defaultValue }) => ({ [key]: defaultValue, ...options }),
getViewOptions(key) ?? {}
)
)
}
const handleOptionsEditorOpened = (event: MouseEvent) => {
showPopup(
ViewOptionsPopup,
{ config, viewOptions: $viewOptionsStore },
eventToHTMLElement(event),
undefined,
(result) => {
if (result?.key === undefined) return
$viewOptionsStore[result.key] = result.value
setViewOptions(viewOptionsKey, $viewOptionsStore)
}
)
}
</script>
<Button
icon={view.icon.ViewButton}
kind={'secondary'}
size={'small'}
showTooltip={{ label: view.string.CustomizeView }}
on:click={handleOptionsEditorOpened}
>
<svelte:fragment slot="content">
<div class="flex-row-center clear-mins pointer-events-none">
<span class="text-sm font-medium"><Label label={view.string.View} /></span>
<div class="icon"><IconDownOutline size={'full'} /></div>
</div>
</svelte:fragment>
</Button>
<style lang="scss">
.icon {
margin-left: 0.25rem;
width: 0.875rem;
height: 0.875rem;
color: var(--content-color);
}
</style>

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