mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
parent
82c8d70d37
commit
d935e9b963
11
changelog.md
11
changelog.md
@ -2,15 +2,23 @@
|
|||||||
|
|
||||||
## 0.6.29 (upcoming)
|
## 0.6.29 (upcoming)
|
||||||
|
|
||||||
|
Platform:
|
||||||
|
|
||||||
|
- Object selector actions
|
||||||
|
|
||||||
Tracker:
|
Tracker:
|
||||||
|
|
||||||
- Remember view options
|
- Remember view options
|
||||||
- My issues
|
- My issues
|
||||||
- Roadmap
|
- Roadmap
|
||||||
|
- Remember view options
|
||||||
|
- Context menus (Priority/Status/Assignee)
|
||||||
|
|
||||||
Chunter:
|
Chunter:
|
||||||
|
|
||||||
- Reactions on messages
|
- Reactions on messages
|
||||||
|
- Priority filter
|
||||||
|
- Context menu selector for state/assignee
|
||||||
|
|
||||||
HR:
|
HR:
|
||||||
|
|
||||||
@ -27,7 +35,6 @@ Tracker:
|
|||||||
- Issue state history.
|
- Issue state history.
|
||||||
- Subissue issue popup.
|
- Subissue issue popup.
|
||||||
- Label support
|
- Label support
|
||||||
- Priority filter
|
|
||||||
|
|
||||||
Lead:
|
Lead:
|
||||||
|
|
||||||
|
@ -114,7 +114,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: attachment.category.Attachments,
|
category: attachment.category.Attachments,
|
||||||
target: attachment.class.Attachment,
|
target: attachment.class.Attachment,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'edit'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -133,7 +134,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: attachment.category.Attachments,
|
category: attachment.category.Attachments,
|
||||||
target: attachment.class.Attachment,
|
target: attachment.class.Attachment,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'edit'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -152,7 +154,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: attachment.category.Attachments,
|
category: attachment.category.Attachments,
|
||||||
target: attachment.class.Attachment,
|
target: attachment.class.Attachment,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'edit'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -283,7 +283,7 @@ export function createModel (builder: Builder): void {
|
|||||||
input: 'any',
|
input: 'any',
|
||||||
category: board.category.Card,
|
category: board.category.Card,
|
||||||
target: board.class.Card,
|
target: board.class.Card,
|
||||||
context: { mode: 'context', application: board.app.Board, group: 'top' }
|
context: { mode: 'context', application: board.app.Board, group: 'create' }
|
||||||
},
|
},
|
||||||
board.action.Open
|
board.action.Open
|
||||||
)
|
)
|
||||||
@ -294,7 +294,9 @@ export function createModel (builder: Builder): void {
|
|||||||
actionProps: {
|
actionProps: {
|
||||||
component: tags.component.TagsEditorPopup,
|
component: tags.component.TagsEditorPopup,
|
||||||
element: view.popup.PositionElementAlignment,
|
element: view.popup.PositionElementAlignment,
|
||||||
value: 'object'
|
fillProps: {
|
||||||
|
_object: 'value'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
label: board.string.Labels,
|
label: board.string.Labels,
|
||||||
icon: tags.icon.Tags,
|
icon: tags.icon.Tags,
|
||||||
@ -302,7 +304,7 @@ export function createModel (builder: Builder): void {
|
|||||||
inline: true,
|
inline: true,
|
||||||
category: board.category.Card,
|
category: board.category.Card,
|
||||||
target: board.class.Card,
|
target: board.class.Card,
|
||||||
context: { mode: 'context', application: board.app.Board, group: 'top' }
|
context: { mode: 'context', application: board.app.Board, group: 'create' }
|
||||||
},
|
},
|
||||||
board.action.Labels
|
board.action.Labels
|
||||||
)
|
)
|
||||||
@ -320,7 +322,7 @@ export function createModel (builder: Builder): void {
|
|||||||
inline: true,
|
inline: true,
|
||||||
category: board.category.Card,
|
category: board.category.Card,
|
||||||
target: board.class.Card,
|
target: board.class.Card,
|
||||||
context: { mode: 'context', application: board.app.Board, group: 'top' }
|
context: { mode: 'context', application: board.app.Board, group: 'create' }
|
||||||
},
|
},
|
||||||
board.action.Dates
|
board.action.Dates
|
||||||
)
|
)
|
||||||
@ -331,7 +333,9 @@ export function createModel (builder: Builder): void {
|
|||||||
actionProps: {
|
actionProps: {
|
||||||
component: board.component.CoverActionPopup,
|
component: board.component.CoverActionPopup,
|
||||||
element: view.popup.PositionElementAlignment,
|
element: view.popup.PositionElementAlignment,
|
||||||
value: 'object'
|
fillProps: {
|
||||||
|
_object: 'value'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
label: board.string.Cover,
|
label: board.string.Cover,
|
||||||
icon: board.icon.Card,
|
icon: board.icon.Card,
|
||||||
@ -339,7 +343,7 @@ export function createModel (builder: Builder): void {
|
|||||||
inline: true,
|
inline: true,
|
||||||
category: board.category.Card,
|
category: board.category.Card,
|
||||||
target: board.class.Card,
|
target: board.class.Card,
|
||||||
context: { mode: 'context', application: board.app.Board, group: 'top' }
|
context: { mode: 'context', application: board.app.Board, group: 'create' }
|
||||||
},
|
},
|
||||||
board.action.Cover
|
board.action.Cover
|
||||||
)
|
)
|
||||||
|
@ -176,7 +176,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: calendar.category.Calendar,
|
category: calendar.category.Calendar,
|
||||||
target: calendar.class.Event,
|
target: calendar.class.Event,
|
||||||
context: {
|
context: {
|
||||||
mode: 'context'
|
mode: 'context',
|
||||||
|
group: 'create'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
calendar.action.SaveEventReminder
|
calendar.action.SaveEventReminder
|
||||||
|
@ -238,7 +238,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: chunter.category.Chunter,
|
category: chunter.category.Chunter,
|
||||||
target: chunter.class.Message,
|
target: chunter.class.Message,
|
||||||
context: {
|
context: {
|
||||||
mode: 'context'
|
mode: 'context',
|
||||||
|
group: 'edit'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chunter.action.MarkUnread
|
chunter.action.MarkUnread
|
||||||
@ -253,7 +254,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: chunter.category.Chunter,
|
category: chunter.category.Chunter,
|
||||||
target: chunter.class.ThreadMessage,
|
target: chunter.class.ThreadMessage,
|
||||||
context: {
|
context: {
|
||||||
mode: 'context'
|
mode: 'context',
|
||||||
|
group: 'edit'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chunter.action.MarkCommentUnread
|
chunter.action.MarkCommentUnread
|
||||||
@ -272,7 +274,8 @@ export function createModel (builder: Builder): void {
|
|||||||
archived: false
|
archived: false
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
mode: 'context'
|
mode: 'context',
|
||||||
|
group: 'tools'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chunter.action.ArchiveChannel
|
chunter.action.ArchiveChannel
|
||||||
@ -291,7 +294,8 @@ export function createModel (builder: Builder): void {
|
|||||||
archived: true
|
archived: true
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
mode: 'context'
|
mode: 'context',
|
||||||
|
group: 'tools'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chunter.action.UnarchiveChannel
|
chunter.action.UnarchiveChannel
|
||||||
@ -307,7 +311,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: chunter.category.Chunter,
|
category: chunter.category.Chunter,
|
||||||
target: chunter.class.DirectMessage,
|
target: chunter.class.DirectMessage,
|
||||||
context: {
|
context: {
|
||||||
mode: 'context'
|
mode: 'context',
|
||||||
|
group: 'edit'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chunter.action.ConvertToPrivate
|
chunter.action.ConvertToPrivate
|
||||||
|
@ -388,7 +388,10 @@ export function createModel (builder: Builder): void {
|
|||||||
createAction(builder, {
|
createAction(builder, {
|
||||||
...actionTemplates.open,
|
...actionTemplates.open,
|
||||||
target: contact.class.Contact,
|
target: contact.class.Contact,
|
||||||
context: { mode: ['browser', 'context'] }
|
context: {
|
||||||
|
mode: ['browser', 'context'],
|
||||||
|
group: 'create'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +270,7 @@ export function createModel (builder: Builder): void {
|
|||||||
input: 'any',
|
input: 'any',
|
||||||
category: hr.category.HR,
|
category: hr.category.HR,
|
||||||
target: hr.class.Department,
|
target: hr.class.Department,
|
||||||
context: { mode: 'context', application: hr.app.HR, group: 'top' }
|
context: { mode: 'context', application: hr.app.HR, group: 'create' }
|
||||||
},
|
},
|
||||||
hr.action.EditDepartment
|
hr.action.EditDepartment
|
||||||
)
|
)
|
||||||
@ -289,7 +289,7 @@ export function createModel (builder: Builder): void {
|
|||||||
_id: { $nin: [hr.ids.Head] }
|
_id: { $nin: [hr.ids.Head] }
|
||||||
},
|
},
|
||||||
target: hr.class.Department,
|
target: hr.class.Department,
|
||||||
context: { mode: 'context', application: hr.app.HR, group: 'top' }
|
context: { mode: 'context', application: hr.app.HR, group: 'create' }
|
||||||
},
|
},
|
||||||
hr.action.DeleteDepartment
|
hr.action.DeleteDepartment
|
||||||
)
|
)
|
||||||
@ -305,7 +305,7 @@ export function createModel (builder: Builder): void {
|
|||||||
input: 'any',
|
input: 'any',
|
||||||
category: hr.category.HR,
|
category: hr.category.HR,
|
||||||
target: hr.class.Request,
|
target: hr.class.Request,
|
||||||
context: { mode: 'context', application: hr.app.HR, group: 'top' }
|
context: { mode: 'context', application: hr.app.HR, group: 'create' }
|
||||||
},
|
},
|
||||||
hr.action.EditRequest
|
hr.action.EditRequest
|
||||||
)
|
)
|
||||||
|
@ -160,7 +160,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: inventory.category.Inventory,
|
category: inventory.category.Inventory,
|
||||||
target: inventory.class.Category,
|
target: inventory.class.Category,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'associate'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ import attachment from '@anticrm/model-attachment'
|
|||||||
import chunter from '@anticrm/model-chunter'
|
import chunter from '@anticrm/model-chunter'
|
||||||
import contact, { TContact } from '@anticrm/model-contact'
|
import contact, { TContact } from '@anticrm/model-contact'
|
||||||
import core from '@anticrm/model-core'
|
import core from '@anticrm/model-core'
|
||||||
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
|
import task, { actionTemplates, TSpaceWithStates, TTask } from '@anticrm/model-task'
|
||||||
import view, { createAction } from '@anticrm/model-view'
|
import view, { createAction, actionTemplates as viewTemplates } from '@anticrm/model-view'
|
||||||
import workbench, { Application } from '@anticrm/model-workbench'
|
import workbench, { Application } from '@anticrm/model-workbench'
|
||||||
import setting from '@anticrm/setting'
|
import setting from '@anticrm/setting'
|
||||||
import lead from './plugin'
|
import lead from './plugin'
|
||||||
@ -67,6 +67,8 @@ export class TCustomer extends TContact implements Customer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
export function createModel (builder: Builder): void {
|
||||||
|
const archiveId = 'archive'
|
||||||
|
|
||||||
builder.createModel(TFunnel, TLead, TCustomer)
|
builder.createModel(TFunnel, TLead, TCustomer)
|
||||||
|
|
||||||
builder.mixin(lead.class.Funnel, core.class.Class, workbench.mixin.SpaceView, {
|
builder.mixin(lead.class.Funnel, core.class.Class, workbench.mixin.SpaceView, {
|
||||||
@ -113,6 +115,15 @@ export function createModel (builder: Builder): void {
|
|||||||
label: lead.string.Customers
|
label: lead.string.Customers
|
||||||
},
|
},
|
||||||
position: 'top'
|
position: 'top'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: archiveId,
|
||||||
|
component: workbench.component.Archive,
|
||||||
|
icon: view.icon.Archive,
|
||||||
|
label: workbench.string.Archive,
|
||||||
|
position: 'bottom',
|
||||||
|
visibleIf: workbench.function.HasArchiveSpaces,
|
||||||
|
spaceClass: lead.class.Funnel
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
spaces: [
|
spaces: [
|
||||||
@ -129,6 +140,22 @@ export function createModel (builder: Builder): void {
|
|||||||
lead.app.Lead
|
lead.app.Lead
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createAction(builder, { ...actionTemplates.archiveSpace, target: lead.class.Funnel })
|
||||||
|
createAction(builder, { ...actionTemplates.unarchiveSpace, target: lead.class.Funnel })
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
...viewTemplates.open,
|
||||||
|
target: lead.class.Funnel,
|
||||||
|
context: {
|
||||||
|
mode: ['browser', 'context'],
|
||||||
|
group: 'create'
|
||||||
|
},
|
||||||
|
action: workbench.actionImpl.Navigate,
|
||||||
|
actionProps: {
|
||||||
|
mode: 'space'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
view.class.Viewlet,
|
view.class.Viewlet,
|
||||||
core.space.Model,
|
core.space.Model,
|
||||||
@ -248,6 +275,9 @@ export function createModel (builder: Builder): void {
|
|||||||
element: 'top',
|
element: 'top',
|
||||||
props: {
|
props: {
|
||||||
preserveCustomer: true
|
preserveCustomer: true
|
||||||
|
},
|
||||||
|
fillProps: {
|
||||||
|
_id: 'customer'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: lead.string.CreateLead,
|
label: lead.string.CreateLead,
|
||||||
@ -255,7 +285,10 @@ export function createModel (builder: Builder): void {
|
|||||||
input: 'focus',
|
input: 'focus',
|
||||||
category: lead.category.Lead,
|
category: lead.category.Lead,
|
||||||
target: contact.class.Contact,
|
target: contact.class.Contact,
|
||||||
context: { mode: ['context', 'browser'] },
|
context: {
|
||||||
|
mode: ['context', 'browser'],
|
||||||
|
group: 'associate'
|
||||||
|
},
|
||||||
override: [lead.action.CreateGlobalLead]
|
override: [lead.action.CreateGlobalLead]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -275,7 +308,8 @@ export function createModel (builder: Builder): void {
|
|||||||
target: core.class.Doc,
|
target: core.class.Doc,
|
||||||
context: {
|
context: {
|
||||||
mode: ['workbench', 'browser'],
|
mode: ['workbench', 'browser'],
|
||||||
application: lead.app.Lead
|
application: lead.app.Lead,
|
||||||
|
group: 'create'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
lead.action.CreateGlobalLead
|
lead.action.CreateGlobalLead
|
||||||
|
@ -394,10 +394,12 @@ export function createModel (builder: Builder): void {
|
|||||||
action: view.actionImpl.ShowPopup,
|
action: view.actionImpl.ShowPopup,
|
||||||
actionProps: {
|
actionProps: {
|
||||||
component: recruit.component.CreateApplication,
|
component: recruit.component.CreateApplication,
|
||||||
_id: 'candidate',
|
|
||||||
element: 'top',
|
element: 'top',
|
||||||
props: {
|
props: {
|
||||||
preserveCandidate: true
|
preserveCandidate: true
|
||||||
|
},
|
||||||
|
fillProps: {
|
||||||
|
_id: 'candidate'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: recruit.string.CreateAnApplication,
|
label: recruit.string.CreateAnApplication,
|
||||||
@ -405,7 +407,10 @@ export function createModel (builder: Builder): void {
|
|||||||
input: 'focus',
|
input: 'focus',
|
||||||
category: recruit.category.Recruit,
|
category: recruit.category.Recruit,
|
||||||
target: contact.class.Person,
|
target: contact.class.Person,
|
||||||
context: { mode: ['context', 'browser'] },
|
context: {
|
||||||
|
mode: ['context', 'browser'],
|
||||||
|
group: 'associate'
|
||||||
|
},
|
||||||
override: [recruit.action.CreateGlobalApplication]
|
override: [recruit.action.CreateGlobalApplication]
|
||||||
})
|
})
|
||||||
createAction(builder, {
|
createAction(builder, {
|
||||||
@ -422,7 +427,8 @@ export function createModel (builder: Builder): void {
|
|||||||
target: core.class.Doc,
|
target: core.class.Doc,
|
||||||
context: {
|
context: {
|
||||||
mode: ['workbench', 'browser'],
|
mode: ['workbench', 'browser'],
|
||||||
application: recruit.app.Recruit
|
application: recruit.app.Recruit,
|
||||||
|
group: 'create'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -440,7 +446,8 @@ export function createModel (builder: Builder): void {
|
|||||||
target: core.class.Doc,
|
target: core.class.Doc,
|
||||||
context: {
|
context: {
|
||||||
mode: ['workbench', 'browser'],
|
mode: ['workbench', 'browser'],
|
||||||
application: recruit.app.Recruit
|
application: recruit.app.Recruit,
|
||||||
|
group: 'create'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -460,7 +467,8 @@ export function createModel (builder: Builder): void {
|
|||||||
target: core.class.Doc,
|
target: core.class.Doc,
|
||||||
context: {
|
context: {
|
||||||
mode: ['workbench', 'browser'],
|
mode: ['workbench', 'browser'],
|
||||||
application: recruit.app.Recruit
|
application: recruit.app.Recruit,
|
||||||
|
group: 'create'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
recruit.action.CreateGlobalApplication
|
recruit.action.CreateGlobalApplication
|
||||||
@ -504,7 +512,8 @@ export function createModel (builder: Builder): void {
|
|||||||
keyBinding: ['e'],
|
keyBinding: ['e'],
|
||||||
target: recruit.class.Vacancy,
|
target: recruit.class.Vacancy,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'create'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -526,12 +535,13 @@ export function createModel (builder: Builder): void {
|
|||||||
|
|
||||||
createReviewModel(builder)
|
createReviewModel(builder)
|
||||||
|
|
||||||
// createAction(builder, { ...viewTemplates.open, target: recruit.class.Vacancy, context: { mode: ['browser', 'context'] } })
|
|
||||||
|
|
||||||
createAction(builder, {
|
createAction(builder, {
|
||||||
...viewTemplates.open,
|
...viewTemplates.open,
|
||||||
target: recruit.class.Vacancy,
|
target: recruit.class.Vacancy,
|
||||||
context: { mode: ['browser', 'context'] },
|
context: {
|
||||||
|
mode: ['browser', 'context'],
|
||||||
|
group: 'create'
|
||||||
|
},
|
||||||
action: workbench.actionImpl.Navigate,
|
action: workbench.actionImpl.Navigate,
|
||||||
actionProps: {
|
actionProps: {
|
||||||
mode: 'space'
|
mode: 'space'
|
||||||
@ -541,7 +551,10 @@ export function createModel (builder: Builder): void {
|
|||||||
createAction(builder, {
|
createAction(builder, {
|
||||||
...viewTemplates.open,
|
...viewTemplates.open,
|
||||||
target: recruit.class.Applicant,
|
target: recruit.class.Applicant,
|
||||||
context: { mode: ['browser', 'context'] }
|
context: {
|
||||||
|
mode: ['browser', 'context'],
|
||||||
|
group: 'create'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function createGotoSpecialAction (builder: Builder, id: string, key: KeyBinding, label: IntlString): void {
|
function createGotoSpecialAction (builder: Builder, id: string, key: KeyBinding, label: IntlString): void {
|
||||||
@ -574,6 +587,55 @@ export function createModel (builder: Builder): void {
|
|||||||
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
|
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
action: view.actionImpl.ValueSelector,
|
||||||
|
actionPopup: view.component.ValueSelector,
|
||||||
|
actionProps: {
|
||||||
|
attribute: 'assignee',
|
||||||
|
_class: contact.class.Employee,
|
||||||
|
query: {},
|
||||||
|
placeholder: recruit.string.AssignRecruiter
|
||||||
|
},
|
||||||
|
label: recruit.string.AssignRecruiter,
|
||||||
|
icon: contact.icon.Person,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'none',
|
||||||
|
category: recruit.category.Recruit,
|
||||||
|
target: recruit.class.Applicant,
|
||||||
|
context: {
|
||||||
|
mode: ['context'],
|
||||||
|
application: recruit.app.Recruit,
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
action: view.actionImpl.ValueSelector,
|
||||||
|
actionPopup: view.component.ValueSelector,
|
||||||
|
actionProps: {
|
||||||
|
attribute: 'state',
|
||||||
|
_class: task.class.State,
|
||||||
|
query: {},
|
||||||
|
searchField: 'title',
|
||||||
|
// should match space
|
||||||
|
fillQuery: { space: 'space' },
|
||||||
|
// Only apply for same vacancy
|
||||||
|
docMatches: ['space'],
|
||||||
|
placeholder: task.string.TaskState
|
||||||
|
},
|
||||||
|
label: task.string.TaskState,
|
||||||
|
icon: task.icon.TaskState,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'none',
|
||||||
|
category: recruit.category.Recruit,
|
||||||
|
target: recruit.class.Applicant,
|
||||||
|
context: {
|
||||||
|
mode: ['context'],
|
||||||
|
application: recruit.app.Recruit,
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { recruitOperation } from './migration'
|
export { recruitOperation } from './migration'
|
||||||
|
@ -49,7 +49,8 @@ export function createReviewModel (builder: Builder): void {
|
|||||||
category: recruit.category.Recruit,
|
category: recruit.category.Recruit,
|
||||||
target: recruit.class.Review,
|
target: recruit.class.Review,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'create'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
recruit.action.CreateOpinion
|
recruit.action.CreateOpinion
|
||||||
@ -75,11 +76,13 @@ export function createReviewModel (builder: Builder): void {
|
|||||||
action: view.actionImpl.ShowPopup,
|
action: view.actionImpl.ShowPopup,
|
||||||
actionProps: {
|
actionProps: {
|
||||||
component: recruit.component.CreateReview,
|
component: recruit.component.CreateReview,
|
||||||
_id: 'candidate',
|
|
||||||
_space: 'space',
|
|
||||||
element: 'top',
|
element: 'top',
|
||||||
props: {
|
props: {
|
||||||
preserveCandidate: true
|
preserveCandidate: true
|
||||||
|
},
|
||||||
|
fillProps: {
|
||||||
|
space: '_space',
|
||||||
|
_id: 'candidate'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: recruit.string.CreateReview,
|
label: recruit.string.CreateReview,
|
||||||
@ -88,7 +91,8 @@ export function createReviewModel (builder: Builder): void {
|
|||||||
category: recruit.category.Recruit,
|
category: recruit.category.Recruit,
|
||||||
target: recruit.mixin.Candidate,
|
target: recruit.mixin.Candidate,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'associate'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -281,7 +281,8 @@ export const actionTemplates = template({
|
|||||||
archived: false
|
archived: false
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'tools'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
unarchiveSpace: {
|
unarchiveSpace: {
|
||||||
@ -301,7 +302,8 @@ export const actionTemplates = template({
|
|||||||
archived: true
|
archived: true
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'tools'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -427,7 +429,8 @@ export function createModel (builder: Builder): void {
|
|||||||
archived: false
|
archived: false
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'edit'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
task.action.EditStatuses
|
task.action.EditStatuses
|
||||||
@ -487,7 +490,8 @@ export function createModel (builder: Builder): void {
|
|||||||
},
|
},
|
||||||
target: task.class.TodoItem,
|
target: task.class.TodoItem,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'edit'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -505,7 +509,8 @@ export function createModel (builder: Builder): void {
|
|||||||
done: true
|
done: true
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'edit'
|
||||||
},
|
},
|
||||||
target: task.class.TodoItem
|
target: task.class.TodoItem
|
||||||
})
|
})
|
||||||
@ -516,7 +521,8 @@ export function createModel (builder: Builder): void {
|
|||||||
...viewTemplates.move,
|
...viewTemplates.move,
|
||||||
target: task.class.Task,
|
target: task.class.Task,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'tools'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
task.action.Move
|
task.action.Move
|
||||||
@ -542,7 +548,8 @@ export function createModel (builder: Builder): void {
|
|||||||
category: task.category.Task,
|
category: task.category.Task,
|
||||||
target: task.class.State,
|
target: task.class.State,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser']
|
mode: ['context', 'browser'],
|
||||||
|
group: 'tools'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
task.action.ArchiveState
|
task.action.ArchiveState
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import type { Employee } from '@anticrm/contact'
|
import type { Employee } from '@anticrm/contact'
|
||||||
import contact from '@anticrm/contact'
|
import contact from '@anticrm/contact'
|
||||||
import { Domain, DOMAIN_MODEL, IndexKind, Markup, Ref, Timestamp, Type } from '@anticrm/core'
|
import { Domain, DOMAIN_MODEL, FindOptions, IndexKind, Markup, Ref, SortingOrder, Timestamp, Type } from '@anticrm/core'
|
||||||
import {
|
import {
|
||||||
ArrOf,
|
ArrOf,
|
||||||
Builder,
|
Builder,
|
||||||
@ -55,6 +55,7 @@ import tags from '@anticrm/tags'
|
|||||||
import tracker from './plugin'
|
import tracker from './plugin'
|
||||||
|
|
||||||
import presentation from '@anticrm/model-presentation'
|
import presentation from '@anticrm/model-presentation'
|
||||||
|
import { defaultPriorities, issuePriorities } from '@anticrm/tracker-resources/src/types'
|
||||||
|
|
||||||
export { trackerOperation } from './migration'
|
export { trackerOperation } from './migration'
|
||||||
export { default } from './plugin'
|
export { default } from './plugin'
|
||||||
@ -565,22 +566,25 @@ export function createModel (builder: Builder): void {
|
|||||||
{
|
{
|
||||||
action: view.actionImpl.ShowPopup,
|
action: view.actionImpl.ShowPopup,
|
||||||
actionProps: {
|
actionProps: {
|
||||||
component: tracker.component.SetDueDateActionPopup,
|
component: tracker.component.CreateIssue,
|
||||||
props: { mondayStart: true, withTime: false },
|
element: 'top',
|
||||||
element: 'top'
|
fillProps: {
|
||||||
|
_object: 'parentIssue'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
label: tracker.string.SetDueDate,
|
label: tracker.string.NewSubIssue,
|
||||||
icon: tracker.icon.DueDate,
|
icon: tracker.icon.Issue,
|
||||||
keyBinding: [],
|
keyBinding: [],
|
||||||
input: 'none',
|
input: 'focus',
|
||||||
category: tracker.category.Tracker,
|
category: tracker.category.Tracker,
|
||||||
target: tracker.class.Issue,
|
target: tracker.class.Issue,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser'],
|
mode: ['context', 'browser'],
|
||||||
application: tracker.app.Tracker
|
application: tracker.app.Tracker,
|
||||||
|
group: 'associate'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tracker.action.SetDueDate
|
tracker.action.SetParent
|
||||||
)
|
)
|
||||||
|
|
||||||
createAction(
|
createAction(
|
||||||
@ -598,13 +602,35 @@ export function createModel (builder: Builder): void {
|
|||||||
category: tracker.category.Tracker,
|
category: tracker.category.Tracker,
|
||||||
target: tracker.class.Issue,
|
target: tracker.class.Issue,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser'],
|
mode: ['context'],
|
||||||
application: tracker.app.Tracker
|
application: tracker.app.Tracker,
|
||||||
|
group: 'associate'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tracker.action.SetParent
|
tracker.action.SetParent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
action: view.actionImpl.ShowPopup,
|
||||||
|
actionPopup: tracker.component.SetParentIssueActionPopup,
|
||||||
|
actionProps: {
|
||||||
|
component: tracker.component.SetParentIssueActionPopup,
|
||||||
|
element: 'top'
|
||||||
|
},
|
||||||
|
label: tracker.string.SetParent,
|
||||||
|
icon: tracker.icon.Parent,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'none',
|
||||||
|
category: tracker.category.Tracker,
|
||||||
|
target: tracker.class.Issue,
|
||||||
|
override: [tracker.action.SetParent],
|
||||||
|
context: {
|
||||||
|
mode: ['browser'],
|
||||||
|
application: tracker.app.Tracker,
|
||||||
|
group: 'associate'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, {
|
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, {
|
||||||
filters: ['status', 'priority', 'assignee', 'project', 'dueDate', 'modifiedOn']
|
filters: ['status', 'priority', 'assignee', 'project', 'dueDate', 'modifiedOn']
|
||||||
})
|
})
|
||||||
@ -619,4 +645,139 @@ export function createModel (builder: Builder): void {
|
|||||||
},
|
},
|
||||||
tracker.completion.IssueCategory
|
tracker.completion.IssueCategory
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const statusOptions: FindOptions<IssueStatus> = {
|
||||||
|
lookup: {
|
||||||
|
category: tracker.class.IssueStatusCategory
|
||||||
|
},
|
||||||
|
sort: { rank: SortingOrder.Ascending }
|
||||||
|
}
|
||||||
|
|
||||||
|
createAction(
|
||||||
|
builder,
|
||||||
|
{
|
||||||
|
action: view.actionImpl.ValueSelector,
|
||||||
|
actionPopup: view.component.ValueSelector,
|
||||||
|
actionProps: {
|
||||||
|
attribute: 'status',
|
||||||
|
_class: tracker.class.IssueStatus,
|
||||||
|
placeholder: tracker.string.SetStatus,
|
||||||
|
fillQuery: {
|
||||||
|
space: 'space'
|
||||||
|
},
|
||||||
|
queryOptions: statusOptions
|
||||||
|
},
|
||||||
|
label: tracker.string.Status,
|
||||||
|
icon: tracker.icon.CategoryBacklog,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'none',
|
||||||
|
category: tracker.category.Tracker,
|
||||||
|
target: tracker.class.Issue,
|
||||||
|
context: {
|
||||||
|
mode: ['context'],
|
||||||
|
application: tracker.app.Tracker,
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tracker.action.SetStatus
|
||||||
|
)
|
||||||
|
createAction(
|
||||||
|
builder,
|
||||||
|
{
|
||||||
|
action: view.actionImpl.ValueSelector,
|
||||||
|
actionPopup: view.component.ValueSelector,
|
||||||
|
actionProps: {
|
||||||
|
attribute: 'priority',
|
||||||
|
values: defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] })),
|
||||||
|
placeholder: tracker.string.SetPriority
|
||||||
|
},
|
||||||
|
label: tracker.string.Priority,
|
||||||
|
icon: tracker.icon.PriorityHigh,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'none',
|
||||||
|
category: tracker.category.Tracker,
|
||||||
|
target: tracker.class.Issue,
|
||||||
|
context: {
|
||||||
|
mode: ['context'],
|
||||||
|
application: tracker.app.Tracker,
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tracker.action.SetPriority
|
||||||
|
)
|
||||||
|
createAction(
|
||||||
|
builder,
|
||||||
|
{
|
||||||
|
action: view.actionImpl.ValueSelector,
|
||||||
|
actionPopup: view.component.ValueSelector,
|
||||||
|
actionProps: {
|
||||||
|
attribute: 'assignee',
|
||||||
|
_class: contact.class.Employee,
|
||||||
|
query: {},
|
||||||
|
placeholder: tracker.string.AssignTo
|
||||||
|
},
|
||||||
|
label: tracker.string.Assignee,
|
||||||
|
icon: contact.icon.Person,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'none',
|
||||||
|
category: tracker.category.Tracker,
|
||||||
|
target: tracker.class.Issue,
|
||||||
|
context: {
|
||||||
|
mode: ['context'],
|
||||||
|
application: tracker.app.Tracker,
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tracker.action.SetAssignee
|
||||||
|
)
|
||||||
|
|
||||||
|
createAction(
|
||||||
|
builder,
|
||||||
|
{
|
||||||
|
action: view.actionImpl.ValueSelector,
|
||||||
|
actionPopup: view.component.ValueSelector,
|
||||||
|
actionProps: {
|
||||||
|
attribute: 'project',
|
||||||
|
_class: tracker.class.Project,
|
||||||
|
query: {},
|
||||||
|
searchField: 'label',
|
||||||
|
placeholder: tracker.string.Project
|
||||||
|
},
|
||||||
|
label: tracker.string.Project,
|
||||||
|
icon: tracker.icon.Project,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'none',
|
||||||
|
category: tracker.category.Tracker,
|
||||||
|
target: tracker.class.Issue,
|
||||||
|
context: {
|
||||||
|
mode: ['context'],
|
||||||
|
application: tracker.app.Tracker,
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tracker.action.SetProject
|
||||||
|
)
|
||||||
|
createAction(
|
||||||
|
builder,
|
||||||
|
{
|
||||||
|
action: view.actionImpl.ShowPopup,
|
||||||
|
actionProps: {
|
||||||
|
component: tracker.component.SetDueDateActionPopup,
|
||||||
|
props: { mondayStart: true, withTime: false },
|
||||||
|
element: 'top'
|
||||||
|
},
|
||||||
|
label: tracker.string.SetDueDate,
|
||||||
|
icon: tracker.icon.DueDate,
|
||||||
|
keyBinding: [],
|
||||||
|
input: 'none',
|
||||||
|
category: tracker.category.Tracker,
|
||||||
|
target: tracker.class.Issue,
|
||||||
|
context: {
|
||||||
|
mode: ['context', 'browser'],
|
||||||
|
application: tracker.app.Tracker,
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tracker.action.SetDueDate
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import { ObjectSearchCategory, ObjectSearchFactory } from '@anticrm/model-presen
|
|||||||
import { IntlString, mergeIds, Resource } from '@anticrm/platform'
|
import { IntlString, mergeIds, Resource } from '@anticrm/platform'
|
||||||
import { trackerId } from '@anticrm/tracker'
|
import { trackerId } from '@anticrm/tracker'
|
||||||
import tracker from '@anticrm/tracker-resources/src/plugin'
|
import tracker from '@anticrm/tracker-resources/src/plugin'
|
||||||
import type { AnyComponent } from '@anticrm/ui'
|
import type { AnyComponent } from '@anticrm/ui/src/types'
|
||||||
import { ViewletDescriptor } from '@anticrm/view'
|
import { ViewletDescriptor } from '@anticrm/view'
|
||||||
import { Application } from '@anticrm/workbench'
|
import { Application } from '@anticrm/workbench'
|
||||||
|
|
||||||
|
@ -44,14 +44,13 @@
|
|||||||
},
|
},
|
||||||
(result) => {
|
(result) => {
|
||||||
objects = result
|
objects = result
|
||||||
|
dispatch('content', objects)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
$: dispatch('content', objects)
|
|
||||||
|
|
||||||
function getStateObjects (
|
function getStateObjects (
|
||||||
objects: Item[],
|
objects: Item[],
|
||||||
state: TypeState,
|
state: TypeState,
|
||||||
|
@ -22,7 +22,9 @@
|
|||||||
createFocusManager,
|
createFocusManager,
|
||||||
EditBox,
|
EditBox,
|
||||||
FocusHandler,
|
FocusHandler,
|
||||||
|
Icon,
|
||||||
IconAdd,
|
IconAdd,
|
||||||
|
IconCheck,
|
||||||
ListView,
|
ListView,
|
||||||
showPopup,
|
showPopup,
|
||||||
Tooltip
|
Tooltip
|
||||||
@ -44,7 +46,8 @@
|
|||||||
export let selectedObjects: Ref<Doc>[] = []
|
export let selectedObjects: Ref<Doc>[] = []
|
||||||
export let ignoreObjects: Ref<Doc>[] = []
|
export let ignoreObjects: Ref<Doc>[] = []
|
||||||
export let shadows: boolean = true
|
export let shadows: boolean = true
|
||||||
export let width: 'medium' | 'large' = 'medium'
|
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||||
|
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||||
|
|
||||||
export let searchField: string = 'name'
|
export let searchField: string = 'name'
|
||||||
|
|
||||||
@ -56,7 +59,7 @@
|
|||||||
label: IntlString
|
label: IntlString
|
||||||
update: (doc: Doc) => string
|
update: (doc: Doc) => string
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined = undefined
|
||||||
|
|
||||||
let search: string = ''
|
let search: string = ''
|
||||||
let objects: Doc[] = []
|
let objects: Doc[] = []
|
||||||
@ -162,7 +165,13 @@
|
|||||||
|
|
||||||
<FocusHandler {manager} />
|
<FocusHandler {manager} />
|
||||||
|
|
||||||
<div class="selectPopup" class:plainContainer={!shadows} class:width-40={width === 'large'} on:keydown={onKeydown}>
|
<div
|
||||||
|
class="selectPopup"
|
||||||
|
class:full-width={width === 'full'}
|
||||||
|
class:plainContainer={!shadows}
|
||||||
|
class:width-40={width === 'large'}
|
||||||
|
on:keydown={onKeydown}
|
||||||
|
>
|
||||||
<div class="header flex-between">
|
<div class="header flex-between">
|
||||||
<EditBox kind={'search-style'} focusIndex={1} focus bind:value={search} {placeholder} />
|
<EditBox kind={'search-style'} focusIndex={1} focus bind:value={search} {placeholder} />
|
||||||
{#if create !== undefined}
|
{#if create !== undefined}
|
||||||
@ -170,7 +179,7 @@
|
|||||||
<Button
|
<Button
|
||||||
focusIndex={2}
|
focusIndex={2}
|
||||||
kind={'transparent'}
|
kind={'transparent'}
|
||||||
size={'small'}
|
{size}
|
||||||
icon={IconAdd}
|
icon={IconAdd}
|
||||||
showTooltip={{ label: create.label }}
|
showTooltip={{ label: create.label }}
|
||||||
on:click={onCreate}
|
on:click={onCreate}
|
||||||
@ -200,23 +209,27 @@
|
|||||||
handleSelection(undefined, objects, item)
|
handleSelection(undefined, objects, item)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if multiSelect}
|
{#if allowDeselect && selected}
|
||||||
<div class="check pointer-events-none">
|
<div class="icon">
|
||||||
<CheckBox checked={selectedElements.has(obj._id)} primary />
|
{#if obj._id === selected}
|
||||||
|
{#if titleDeselect}
|
||||||
|
<Tooltip label={titleDeselect ?? presentation.string.Deselect}>
|
||||||
|
<Icon icon={IconCheck} {size} />
|
||||||
|
</Tooltip>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={IconCheck} {size} />
|
||||||
|
<!-- <CheckBox checked circle primary /> -->
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<slot name="item" item={obj} />
|
<span class="label">
|
||||||
|
<slot name="item" item={obj} />
|
||||||
{#if allowDeselect && obj._id === selected}
|
</span>
|
||||||
<div class="check-right pointer-events-none">
|
{#if multiSelect}
|
||||||
{#if titleDeselect}
|
<div class="check pointer-events-none">
|
||||||
<Tooltip label={titleDeselect ?? presentation.string.Deselect}>
|
<CheckBox checked={selectedElements.has(obj._id)} primary />
|
||||||
<CheckBox checked circle primary />
|
|
||||||
</Tooltip>
|
|
||||||
{:else}
|
|
||||||
<CheckBox checked circle primary />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
@ -25,6 +25,15 @@
|
|||||||
background: var(--popup-bg-color);
|
background: var(--popup-bg-color);
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
box-shadow: var(--popup-shadow);
|
box-shadow: var(--popup-shadow);
|
||||||
|
|
||||||
|
&.full-width {
|
||||||
|
flex-grow: 1;
|
||||||
|
background: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&.maxHeight { height: 22rem; }
|
&.maxHeight { height: 22rem; }
|
||||||
&.autoHeight {
|
&.autoHeight {
|
||||||
@ -242,11 +251,11 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
.icon { color: var(--content-color); }
|
.icon { color: var(--content-color); }
|
||||||
&:focus .icon { color: var(--accent-color); }
|
&.hover .icon { color: var(--accent-color); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// &:hover { background-color: var(--popup-bg-hover); }
|
// &:hover { background-color: var(--popup-bg-hover); }
|
||||||
&:focus {
|
&.hover {
|
||||||
background-color: var(--popup-bg-hover);
|
background-color: var(--popup-bg-hover);
|
||||||
outline: none;
|
outline: none;
|
||||||
// box-shadow: inset 0 0 1px 1px var(--primary-edit-border-color);
|
// box-shadow: inset 0 0 1px 1px var(--primary-edit-border-color);
|
||||||
@ -258,6 +267,12 @@
|
|||||||
&:active { color: var(--highlight-red-press); }
|
&:active { color: var(--highlight-red-press); }
|
||||||
&:focus { color: var(--highlight-red-press); }
|
&:focus { color: var(--highlight-red-press); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.separator {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--theme-card-divider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.ap-check {
|
.ap-check {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
@ -360,7 +375,9 @@
|
|||||||
.selectPopup .menu-item.arrow,
|
.selectPopup .menu-item.arrow,
|
||||||
.antiPopup-submenu {
|
.antiPopup-submenu {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
span {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
&::after {
|
&::after {
|
||||||
content: '▶';
|
content: '▶';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"DaysAfter": "{days, plural, =1 {in a day} other {in # days}}",
|
"DaysAfter": "{days, plural, =1 {in a day} other {in # days}}",
|
||||||
"NoActionsDefined": "No actions applicable",
|
"NoActionsDefined": "No actions applicable",
|
||||||
"Incoming": "Incoming",
|
"Incoming": "Incoming",
|
||||||
"HoursLabel": "Hours"
|
"HoursLabel": "Hours",
|
||||||
|
"Back": "Back"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"DaysAfter": "{days, plural, =1 {через день} other {через # дней}}",
|
"DaysAfter": "{days, plural, =1 {через день} other {через # дней}}",
|
||||||
"NoActionsDefined": "Нет доступных действий",
|
"NoActionsDefined": "Нет доступных действий",
|
||||||
"Incoming": "Входящие",
|
"Incoming": "Входящие",
|
||||||
"HoursLabel": "Часы"
|
"HoursLabel": "Часы",
|
||||||
|
"Back": "Назад"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,76 +13,148 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
|
import { afterUpdate, createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
import ui from '../plugin'
|
import ui from '../plugin'
|
||||||
|
import { closePopup, showPopup } from '../popups'
|
||||||
import { Action } from '../types'
|
import { Action } from '../types'
|
||||||
import Icon from './Icon.svelte'
|
import Icon from './Icon.svelte'
|
||||||
import Label from './Label.svelte'
|
import Label from './Label.svelte'
|
||||||
|
import MouseSpeedTracker from './MouseSpeedTracker.svelte'
|
||||||
|
|
||||||
export let actions: Action[] = []
|
export let actions: Action[] = []
|
||||||
export let ctx: any = undefined
|
export let ctx: any = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const btns: HTMLButtonElement[] = []
|
const btns: HTMLElement[] = []
|
||||||
|
let activeElement: HTMLElement
|
||||||
|
|
||||||
const keyDown = (ev: KeyboardEvent, n: number): void => {
|
const keyDown = (ev: KeyboardEvent): void => {
|
||||||
if (ev.key === 'Tab') {
|
if (ev.key === 'Tab') {
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
}
|
}
|
||||||
|
const n = btns.indexOf(activeElement) ?? 0
|
||||||
if (ev.key === 'ArrowDown') {
|
if (ev.key === 'ArrowDown') {
|
||||||
if (n === btns.length - 1) btns[0].focus()
|
if (n < btns.length - 1) {
|
||||||
else btns[n + 1].focus()
|
activeElement = btns[n + 1]
|
||||||
|
}
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
}
|
}
|
||||||
if (ev.key === 'ArrowUp') {
|
if (ev.key === 'ArrowUp') {
|
||||||
if (n === 0) btns[btns.length - 1].focus()
|
if (n > 0) {
|
||||||
else btns[n - 1].focus()
|
activeElement = btns[n - 1]
|
||||||
|
}
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
}
|
||||||
|
if (ev.key === 'ArrowLeft') {
|
||||||
|
dispatch('update', 'left')
|
||||||
|
closePopup('submenu')
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
}
|
||||||
|
if (ev.key === 'ArrowRight') {
|
||||||
|
dispatch('update', 'right')
|
||||||
|
showActionPopup(actions[n], activeElement)
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
}
|
}
|
||||||
if (ev.key === 'ArrowLeft' && ev.altKey) dispatch('update', 'left')
|
|
||||||
if (ev.key === 'ArrowRight' && ev.altKey) dispatch('update', 'right')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
dispatch('update', Date.now())
|
dispatch('update', Date.now())
|
||||||
})
|
})
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (btns[0]) btns[0].focus()
|
if (btns[0]) {
|
||||||
|
btns[0].focus()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
onDestroy(() => {
|
||||||
|
closePopup('submenu')
|
||||||
|
})
|
||||||
|
|
||||||
|
function showActionPopup (action: Action, target: HTMLElement): void {
|
||||||
|
closePopup('submenu')
|
||||||
|
if (action.component !== undefined) {
|
||||||
|
console.log(action.props)
|
||||||
|
showPopup(
|
||||||
|
action.component,
|
||||||
|
action.props,
|
||||||
|
{ getBoundingClientRect: () => target.getBoundingClientRect(), position: { v: 'top', h: 'right' } },
|
||||||
|
(evt) => {
|
||||||
|
dispatch('close')
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ category: 'submenu', overlay: false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function focusTarget (action: Action, target: HTMLElement): void {
|
||||||
|
if (focusSpeed && target !== activeElement) {
|
||||||
|
activeElement = target
|
||||||
|
showActionPopup(action, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusSpeed: boolean = false
|
||||||
|
let popup: HTMLElement
|
||||||
|
|
||||||
|
$: popup?.focus()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="antiPopup">
|
<div class="antiPopup" on:keydown={keyDown}>
|
||||||
|
<MouseSpeedTracker bind:focusSpeed />
|
||||||
<div class="ap-space" />
|
<div class="ap-space" />
|
||||||
<div class="ap-scroll">
|
<div class="ap-scroll">
|
||||||
<div class="ap-box">
|
<div class="ap-box" bind:this={popup}>
|
||||||
{#if actions.length === 0}
|
{#if actions.length === 0}
|
||||||
<div class="p-6 error-color">
|
<div class="p-6 error-color">
|
||||||
<Label label={ui.string.NoActionsDefined} />
|
<Label label={ui.string.NoActionsDefined} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each actions as action, i}
|
{#each actions as action, i}
|
||||||
|
{#if i > 0 && actions[i - 1].group !== action.group}
|
||||||
|
<span class="ap-menuItem separator" />
|
||||||
|
{/if}
|
||||||
{#if action.link}
|
{#if action.link}
|
||||||
<a class="stealth" href={action.link}>
|
<a class="stealth" href={action.link}>
|
||||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||||
<button
|
<button
|
||||||
bind:this={btns[i]}
|
bind:this={btns[i]}
|
||||||
class="ap-menuItem flex-row-center withIcon w-full"
|
class="ap-menuItem flex-row-center withIcon w-full"
|
||||||
on:keydown={(evt) => keyDown(evt, i)}
|
class:hover={btns[i] === activeElement}
|
||||||
on:mouseover={(evt) => evt.currentTarget.focus()}
|
on:mouseover={(evt) => focusTarget(action, btns[i])}
|
||||||
on:click|preventDefault|stopPropagation={(evt) => {
|
on:click|preventDefault|stopPropagation={(evt) => {
|
||||||
if (!action.inline) dispatch('close')
|
if (!action.inline) dispatch('close')
|
||||||
action.action(ctx, evt)
|
action.action(ctx, evt)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if action.icon}<div class="icon mr-3"><Icon icon={action.icon} size={'small'} /></div>{/if}
|
{#if action.icon}<div class="icon mr-3"><Icon icon={action.icon} size={'small'} /></div>{/if}
|
||||||
<span class="overflow-label pr-1"><Label label={action.label} /></span>
|
<span class="overflow-label pr-1 flex-grow"><Label label={action.label} /></span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
{:else if action.component !== undefined}
|
||||||
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||||
|
<button
|
||||||
|
bind:this={btns[i]}
|
||||||
|
class="ap-menuItem antiPopup-submenu"
|
||||||
|
class:hover={btns[i] === activeElement}
|
||||||
|
on:mouseover={() => focusTarget(action, btns[i])}
|
||||||
|
>
|
||||||
|
{#if action.icon}
|
||||||
|
<div class="icon mr-3"><Icon icon={action.icon} size={'small'} /></div>
|
||||||
|
{/if}
|
||||||
|
<span class="overflow-label pr-1 flex-grow"><Label label={action.label} /></span>
|
||||||
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||||
<button
|
<button
|
||||||
bind:this={btns[i]}
|
bind:this={btns[i]}
|
||||||
class="ap-menuItem flex-row-center withIcon"
|
class="ap-menuItem flex-row-center withIcon"
|
||||||
on:keydown={(evt) => keyDown(evt, i)}
|
class:hover={btns[i] === activeElement}
|
||||||
on:mouseover={(evt) => evt.currentTarget.focus()}
|
on:mouseover={() => focusTarget(action, btns[i])}
|
||||||
on:click={(evt) => {
|
on:click={(evt) => {
|
||||||
if (!action.inline) dispatch('close')
|
if (!action.inline) dispatch('close')
|
||||||
action.action(ctx, evt)
|
action.action(ctx, evt)
|
||||||
@ -91,7 +163,7 @@
|
|||||||
{#if action.icon}
|
{#if action.icon}
|
||||||
<div class="icon mr-3"><Icon icon={action.icon} size={'small'} /></div>
|
<div class="icon mr-3"><Icon icon={action.icon} size={'small'} /></div>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="overflow-label pr-1"><Label label={action.label} /></span>
|
<span class="overflow-label pr-1 flex-grow"><Label label={action.label} /></span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
68
packages/ui/src/components/MouseSpeedTracker.svelte
Normal file
68
packages/ui/src/components/MouseSpeedTracker.svelte
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
import { readable } from 'svelte/store'
|
||||||
|
const ticker = readable(Date.now(), (set) => {
|
||||||
|
setInterval(() => {
|
||||||
|
set(Date.now())
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let focusSpeed: boolean
|
||||||
|
|
||||||
|
let timestamp: number = 0
|
||||||
|
let lastMouseX: number = 0
|
||||||
|
let lastMouseY: number = 0
|
||||||
|
|
||||||
|
let speedX: number
|
||||||
|
let speedY: number
|
||||||
|
let speedD: number
|
||||||
|
|
||||||
|
let maxSpeedX: number
|
||||||
|
let maxSpeedY: number
|
||||||
|
let maxSpeedD: number
|
||||||
|
|
||||||
|
$: focusSpeed = speedD < 50
|
||||||
|
|
||||||
|
function update (now: number) {
|
||||||
|
maxSpeedY = 0
|
||||||
|
maxSpeedX = 0
|
||||||
|
maxSpeedD = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$: update($ticker)
|
||||||
|
|
||||||
|
function trackMouse (evt: MouseEvent): void {
|
||||||
|
if (timestamp === 0) {
|
||||||
|
timestamp = Date.now()
|
||||||
|
lastMouseX = evt.screenX
|
||||||
|
lastMouseY = evt.screenY
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const dt = now - timestamp
|
||||||
|
const dx = evt.screenX - lastMouseX
|
||||||
|
const dy = evt.screenY - lastMouseY
|
||||||
|
speedX = Math.round((dx / dt) * 100)
|
||||||
|
speedY = Math.round((dy / dt) * 100)
|
||||||
|
|
||||||
|
speedD = Math.round(Math.sqrt(speedX * speedX + speedY * speedY))
|
||||||
|
|
||||||
|
if (speedX > maxSpeedX) {
|
||||||
|
maxSpeedX = speedX
|
||||||
|
}
|
||||||
|
if (speedY > maxSpeedY) {
|
||||||
|
maxSpeedY = speedY
|
||||||
|
}
|
||||||
|
if (speedD > maxSpeedD) {
|
||||||
|
maxSpeedD = speedD
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp = now
|
||||||
|
lastMouseX = evt.screenX
|
||||||
|
lastMouseY = evt.screenY
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:mousemove={trackMouse} />
|
@ -30,5 +30,6 @@
|
|||||||
zIndex={(i + 1) * 500}
|
zIndex={(i + 1) * 500}
|
||||||
top={$modal.length - 1 === i}
|
top={$modal.length - 1 === i}
|
||||||
close={popup.close}
|
close={popup.close}
|
||||||
|
overlay={popup.options.overlay}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
export let element: PopupAlignment | undefined
|
export let element: PopupAlignment | undefined
|
||||||
export let onClose: ((result: any) => void) | undefined
|
export let onClose: ((result: any) => void) | undefined
|
||||||
export let onUpdate: ((result: any) => void) | undefined
|
export let onUpdate: ((result: any) => void) | undefined
|
||||||
|
export let overlay: boolean
|
||||||
export let zIndex: number
|
export let zIndex: number
|
||||||
export let top: boolean
|
export let top: boolean
|
||||||
export let close: () => void
|
export let close: () => void
|
||||||
@ -126,12 +127,16 @@
|
|||||||
on:changeContent={fitPopup}
|
on:changeContent={fitPopup}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="modal-overlay"
|
{#if overlay}
|
||||||
class:antiOverlay={options.showOverlay}
|
<div
|
||||||
style={`z-index: ${zIndex};`}
|
class="modal-overlay"
|
||||||
on:click={() => escapeClose()}
|
class:antiOverlay={options.showOverlay}
|
||||||
/>
|
style={`z-index: ${zIndex};`}
|
||||||
|
on:click={() => escapeClose()}
|
||||||
|
on:keydown|stopPropagation|preventDefault={() => {}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.popup {
|
.popup {
|
||||||
|
@ -14,64 +14,124 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Asset, IntlString } from '@anticrm/platform'
|
import type { Asset, IntlString } from '@anticrm/platform'
|
||||||
import { translate } from '@anticrm/platform'
|
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import IconCheck from './icons/Check.svelte'
|
import { createFocusManager } from '../focus'
|
||||||
|
import EditBox from './EditBox.svelte'
|
||||||
|
import FocusHandler from './FocusHandler.svelte'
|
||||||
import Icon from './Icon.svelte'
|
import Icon from './Icon.svelte'
|
||||||
|
import IconCheck from './icons/Check.svelte'
|
||||||
import Label from './Label.svelte'
|
import Label from './Label.svelte'
|
||||||
|
import ListView from './ListView.svelte'
|
||||||
|
|
||||||
|
interface ValueType {
|
||||||
|
id: number | string
|
||||||
|
icon?: Asset
|
||||||
|
label?: IntlString
|
||||||
|
text?: string
|
||||||
|
isSelected?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export let placeholder: IntlString | undefined = undefined
|
export let placeholder: IntlString | undefined = undefined
|
||||||
export let placeholderParam: any | undefined = undefined
|
export let placeholderParam: any | undefined = undefined
|
||||||
export let searchable: boolean = false
|
export let searchable: boolean = false
|
||||||
export let value: Array<{ id: number | string; icon: Asset; label?: IntlString; text?: string; isSelected?: boolean }>
|
export let value: Array<ValueType>
|
||||||
export let width: 'medium' | 'large' = 'medium'
|
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||||
|
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||||
|
|
||||||
let search: string = ''
|
let search: string = ''
|
||||||
|
|
||||||
let phTraslate: string = ''
|
|
||||||
$: if (placeholder) {
|
|
||||||
translate(placeholder, placeholderParam ?? {}).then((res) => {
|
|
||||||
phTraslate = res
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: hasSelected = value.some((v) => v.isSelected)
|
$: hasSelected = value.some((v) => v.isSelected)
|
||||||
|
|
||||||
|
let selection = 0
|
||||||
|
let list: ListView
|
||||||
|
|
||||||
|
function onKeydown (key: KeyboardEvent): void {
|
||||||
|
if (key.code === 'ArrowUp') {
|
||||||
|
key.stopPropagation()
|
||||||
|
key.preventDefault()
|
||||||
|
list.select(selection - 1)
|
||||||
|
}
|
||||||
|
if (key.code === 'ArrowDown') {
|
||||||
|
key.stopPropagation()
|
||||||
|
key.preventDefault()
|
||||||
|
list.select(selection + 1)
|
||||||
|
}
|
||||||
|
if (key.code === 'Enter') {
|
||||||
|
key.preventDefault()
|
||||||
|
key.stopPropagation()
|
||||||
|
dispatch('close', value[selection].id)
|
||||||
|
}
|
||||||
|
if (key.code === 'Escape') {
|
||||||
|
key.preventDefault()
|
||||||
|
key.stopPropagation()
|
||||||
|
dispatch('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const manager = createFocusManager()
|
||||||
|
|
||||||
|
$: filteredObjects = value.filter((el) => (el.label ?? el.text ?? '').toLowerCase().includes(search.toLowerCase()))
|
||||||
|
|
||||||
|
$: huge = size === 'medium' || size === 'large'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="selectPopup" class:max-width-40={width === 'large'}>
|
<FocusHandler {manager} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="selectPopup"
|
||||||
|
class:full-width={width === 'full'}
|
||||||
|
class:max-width-40={width === 'large'}
|
||||||
|
on:keydown={onKeydown}
|
||||||
|
>
|
||||||
{#if searchable}
|
{#if searchable}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<input type="text" bind:value={search} placeholder={phTraslate} on:input={(ev) => {}} on:change />
|
<EditBox
|
||||||
|
kind={'search-style'}
|
||||||
|
focusIndex={1}
|
||||||
|
focus
|
||||||
|
bind:value={search}
|
||||||
|
{placeholder}
|
||||||
|
{placeholderParam}
|
||||||
|
on:change
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
{#each value.filter((el) => (el.label ?? el.text ?? '').toLowerCase().includes(search.toLowerCase())) as item}
|
<ListView bind:this={list} count={filteredObjects.length} bind:selection>
|
||||||
<button
|
<svelte:fragment slot="item" let:item={itemId}>
|
||||||
class="menu-item"
|
{@const item = filteredObjects[itemId]}
|
||||||
on:click={() => dispatch('close', item.id)}
|
<button
|
||||||
on:focus={() => dispatch('update', item)}
|
class="menu-item w-full"
|
||||||
on:mouseover={() => dispatch('update', item)}
|
on:click={() => dispatch('close', item.id)}
|
||||||
>
|
on:focus={() => dispatch('update', item)}
|
||||||
{#if hasSelected}
|
on:mouseover={() => dispatch('update', item)}
|
||||||
<div class="icon">
|
>
|
||||||
{#if item.isSelected}
|
<div class="flex-row-center" class:mt-2={huge} class:mb-2={huge}>
|
||||||
<Icon icon={IconCheck} size={'small'} />
|
{#if hasSelected}
|
||||||
|
<div class="icon">
|
||||||
|
{#if item.isSelected}
|
||||||
|
<Icon icon={IconCheck} {size} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if item.icon}
|
||||||
|
<div class="mr-2">
|
||||||
|
<Icon icon={item.icon} {size} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<span class="label" class:text-base={huge}>
|
||||||
|
{#if item.label}
|
||||||
|
<Label label={item.label} />
|
||||||
|
{:else if item.text}
|
||||||
|
<span>{item.text}</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</button>
|
||||||
<div class="icon"><Icon icon={item.icon} size={'small'} /></div>
|
</svelte:fragment>
|
||||||
<span class="label">
|
</ListView>
|
||||||
{#if item.label}
|
|
||||||
<Label label={item.label} />
|
|
||||||
{:else if item.text}
|
|
||||||
<span>{item.text}</span>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,7 +63,8 @@ export const uis = plugin(uiId, {
|
|||||||
DaysAfter: '' as IntlString,
|
DaysAfter: '' as IntlString,
|
||||||
NoActionsDefined: '' as IntlString,
|
NoActionsDefined: '' as IntlString,
|
||||||
Incoming: '' as IntlString,
|
Incoming: '' as IntlString,
|
||||||
HoursLabel: '' as IntlString
|
HoursLabel: '' as IntlString,
|
||||||
|
Back: '' as IntlString
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
DefaultApplication: '' as Metadata<AnyComponent>
|
DefaultApplication: '' as Metadata<AnyComponent>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import type {
|
|
||||||
AnySvelteComponent,
|
|
||||||
AnyComponent,
|
|
||||||
HorizontalAlignment,
|
|
||||||
PopupAlignment,
|
|
||||||
PopupPositionElement,
|
|
||||||
PopupOptions,
|
|
||||||
VerticalAlignment
|
|
||||||
} from './types'
|
|
||||||
import { getResource } from '@anticrm/platform'
|
import { getResource } from '@anticrm/platform'
|
||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
|
import type {
|
||||||
|
AnyComponent,
|
||||||
|
AnySvelteComponent,
|
||||||
|
HorizontalAlignment,
|
||||||
|
PopupAlignment,
|
||||||
|
PopupOptions,
|
||||||
|
PopupPositionElement,
|
||||||
|
VerticalAlignment
|
||||||
|
} from './types'
|
||||||
|
|
||||||
interface CompAndProps {
|
interface CompAndProps {
|
||||||
id: string
|
id: string
|
||||||
@ -18,9 +18,14 @@ interface CompAndProps {
|
|||||||
onClose?: (result: any) => void
|
onClose?: (result: any) => void
|
||||||
onUpdate?: (result: any) => void
|
onUpdate?: (result: any) => void
|
||||||
close: () => void
|
close: () => void
|
||||||
|
options: {
|
||||||
|
category: string
|
||||||
|
overlay: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const popupstore = writable<CompAndProps[]>([])
|
export const popupstore = writable<CompAndProps[]>([])
|
||||||
|
|
||||||
function addPopup (props: CompAndProps): void {
|
function addPopup (props: CompAndProps): void {
|
||||||
popupstore.update((popups) => {
|
popupstore.update((popups) => {
|
||||||
popups.push(props)
|
popups.push(props)
|
||||||
@ -33,7 +38,11 @@ export function showPopup (
|
|||||||
props: any,
|
props: any,
|
||||||
element?: PopupAlignment,
|
element?: PopupAlignment,
|
||||||
onClose?: (result: any) => void,
|
onClose?: (result: any) => void,
|
||||||
onUpdate?: (result: any) => void
|
onUpdate?: (result: any) => void,
|
||||||
|
options: {
|
||||||
|
category: string
|
||||||
|
overlay: boolean
|
||||||
|
} = { category: 'popup', overlay: true }
|
||||||
): () => void {
|
): () => void {
|
||||||
const id = `${popupId++}`
|
const id = `${popupId++}`
|
||||||
const closePopupOp = (): void => {
|
const closePopupOp = (): void => {
|
||||||
@ -47,17 +56,23 @@ export function showPopup (
|
|||||||
}
|
}
|
||||||
if (typeof component === 'string') {
|
if (typeof component === 'string') {
|
||||||
getResource(component)
|
getResource(component)
|
||||||
.then((resolved) => addPopup({ id, is: resolved, props, element, onClose, onUpdate, close: closePopupOp }))
|
.then((resolved) =>
|
||||||
|
addPopup({ id, is: resolved, props, element, onClose, onUpdate, close: closePopupOp, options })
|
||||||
|
)
|
||||||
.catch((err) => console.log(err))
|
.catch((err) => console.log(err))
|
||||||
} else {
|
} else {
|
||||||
addPopup({ id, is: component, props, element, onClose, onUpdate, close: closePopupOp })
|
addPopup({ id, is: component, props, element, onClose, onUpdate, close: closePopupOp, options })
|
||||||
}
|
}
|
||||||
return closePopupOp
|
return closePopupOp
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closePopup (): void {
|
export function closePopup (category?: string): void {
|
||||||
popupstore.update((popups) => {
|
popupstore.update((popups) => {
|
||||||
popups.pop()
|
if (category !== undefined) {
|
||||||
|
popups = popups.filter((p) => p.options.category !== category)
|
||||||
|
} else {
|
||||||
|
popups.pop()
|
||||||
|
}
|
||||||
return popups
|
return popups
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -179,15 +194,6 @@ export function fitPopupPositionedElement (
|
|||||||
return { props: newProps, showOverlay: false, direction }
|
return { props: newProps, showOverlay: false, direction }
|
||||||
}
|
}
|
||||||
|
|
||||||
// function applyStyle (values: Record<string, string | number>, modalHTML: HTMLElement): void {
|
|
||||||
// for (const [k, v] of Object.entries(values)) {
|
|
||||||
// const old = (modalHTML.style as any)[k]
|
|
||||||
// if (old !== v) {
|
|
||||||
// ;(modalHTML.style as any)[k] = v
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
|
@ -45,6 +45,12 @@ export interface Action {
|
|||||||
action: (props: any, ev: Event) => Promise<void>
|
action: (props: any, ev: Event) => Promise<void>
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
link?: string
|
link?: string
|
||||||
|
|
||||||
|
// Submenu component
|
||||||
|
component?: AnyComponent
|
||||||
|
props?: Record<string, any>
|
||||||
|
|
||||||
|
group?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPopupItem {
|
export interface IPopupItem {
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
export let shouldShowPlaceholder = false
|
export let shouldShowPlaceholder = false
|
||||||
export let onEmployeeEdit: ((event: MouseEvent) => void) | undefined = undefined
|
export let onEmployeeEdit: ((event: MouseEvent) => void) | undefined = undefined
|
||||||
export let avatarSize: 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' = 'x-small'
|
export let avatarSize: 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' = 'x-small'
|
||||||
|
export let isInteractive = true
|
||||||
|
export let inline = false
|
||||||
|
|
||||||
let container: HTMLElement
|
let container: HTMLElement
|
||||||
|
|
||||||
@ -35,15 +37,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={container} class="inline-flex clear-mins">
|
<div bind:this={container} class="inline-flex clear-mins">
|
||||||
<div class="over-underline">
|
<div class:over-underline={!inline}>
|
||||||
<PersonPresenter
|
<PersonPresenter
|
||||||
{value}
|
{value}
|
||||||
{tooltipLabels}
|
{tooltipLabels}
|
||||||
onEdit={handlePersonEdit}
|
onEdit={isInteractive ? handlePersonEdit : () => {}}
|
||||||
{shouldShowAvatar}
|
{shouldShowAvatar}
|
||||||
{shouldShowName}
|
{shouldShowName}
|
||||||
{avatarSize}
|
{avatarSize}
|
||||||
{shouldShowPlaceholder}
|
{shouldShowPlaceholder}
|
||||||
|
{isInteractive}
|
||||||
|
{inline}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if value?.$lookup?.statuses?.length}
|
{#if value?.$lookup?.statuses?.length}
|
||||||
|
@ -153,7 +153,8 @@
|
|||||||
"DurDays": "{days, plural, =0 {today} =1 {1 day} other {# days }}",
|
"DurDays": "{days, plural, =0 {today} =1 {1 day} other {# days }}",
|
||||||
"DurMonths": "{months, plural, =0 {this month} =1 {1 month} other {# months}}",
|
"DurMonths": "{months, plural, =0 {this month} =1 {1 month} other {# months}}",
|
||||||
"DurYears": "{years, plural, =0 {this year} =1 {a year} other {# years}}",
|
"DurYears": "{years, plural, =0 {this year} =1 {a year} other {# years}}",
|
||||||
"StatusHistory": "State History"
|
"StatusHistory": "State History",
|
||||||
|
"NewSubIssue": "Add sub-issue..."
|
||||||
},
|
},
|
||||||
"status": {}
|
"status": {}
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,8 @@
|
|||||||
"DurDays": "{days, plural, =0 {сегодня} =1 {1 день} =2 {2 дня} =3 {3 дня} =4 {4 дня} other {# дней }}",
|
"DurDays": "{days, plural, =0 {сегодня} =1 {1 день} =2 {2 дня} =3 {3 дня} =4 {4 дня} other {# дней }}",
|
||||||
"DurMonths": "{months, plural, =0 {меньше месяця} =1 {месяц} =2 {2 месяца} =3 {3 месяца} =4 {4 месяца} other {# месяцев}}",
|
"DurMonths": "{months, plural, =0 {меньше месяця} =1 {месяц} =2 {2 месяца} =3 {3 месяца} =4 {4 месяца} other {# месяцев}}",
|
||||||
"DurYears": "{years, plural, =0 {меньше года} =1 {год} =2 {2 года} =3 {3 года} =4 {4 года} other {# лет}}",
|
"DurYears": "{years, plural, =0 {меньше года} =1 {год} =2 {2 года} =3 {3 года} =4 {4 года} other {# лет}}",
|
||||||
"StatusHistory": "История состояний"
|
"StatusHistory": "История состояний",
|
||||||
|
"NewSubIssue": "Добавить под-задачу..."
|
||||||
},
|
},
|
||||||
"status": {}
|
"status": {}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
let currentAssignee: Ref<Employee> | null = assignee
|
let currentAssignee: Ref<Employee> | null = assignee
|
||||||
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||||
let parentIssue: Issue | undefined
|
export let parentIssue: Issue | undefined
|
||||||
let labels: TagReference[] = []
|
let labels: TagReference[] = []
|
||||||
|
|
||||||
let objectId: Ref<Issue> = generateId()
|
let objectId: Ref<Issue> = generateId()
|
||||||
|
@ -14,24 +14,24 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AttachedData, FindOptions, SortingOrder } from '@anticrm/core'
|
import { AttachedData, FindOptions, SortingOrder } from '@anticrm/core'
|
||||||
import { Issue, IssueStatusCategory, Team, calcRank } from '@anticrm/tracker'
|
import { getClient, ObjectPopup } from '@anticrm/presentation'
|
||||||
import { createQuery, getClient, ObjectPopup } from '@anticrm/presentation'
|
import { calcRank, Issue, IssueStatusCategory } from '@anticrm/tracker'
|
||||||
import { Icon } from '@anticrm/ui'
|
import { Icon } from '@anticrm/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import tracker from '../plugin'
|
import tracker from '../plugin'
|
||||||
import { getIssueId } from '../utils'
|
import { getIssueId } from '../utils'
|
||||||
|
|
||||||
export let value: Issue | AttachedData<Issue>
|
export let value: Issue | AttachedData<Issue> | Issue[]
|
||||||
|
export let width: 'medium' | 'large' | 'full' = 'large'
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const spaceQuery = createQuery()
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const options: FindOptions<Issue> = {
|
const options: FindOptions<Issue> = {
|
||||||
lookup: { status: tracker.class.IssueStatus },
|
lookup: { status: tracker.class.IssueStatus, space: tracker.class.Team },
|
||||||
sort: { modifiedOn: SortingOrder.Descending }
|
sort: { modifiedOn: SortingOrder.Descending }
|
||||||
}
|
}
|
||||||
|
|
||||||
let team: Team | undefined
|
|
||||||
let statusCategoryById: Map<string, IssueStatusCategory> | undefined
|
let statusCategoryById: Map<string, IssueStatusCategory> | undefined
|
||||||
|
|
||||||
async function updateIssueStatusCategories () {
|
async function updateIssueStatusCategories () {
|
||||||
@ -41,32 +41,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onClose ({ detail: parentIssue }: CustomEvent<Issue | undefined | null>) {
|
async function onClose ({ detail: parentIssue }: CustomEvent<Issue | undefined | null>) {
|
||||||
if ('_id' in value && parentIssue !== undefined && parentIssue?._id !== value.attachedTo) {
|
const vv = Array.isArray(value) ? value : [value]
|
||||||
let rank: string | null = null
|
for (const docValue of vv) {
|
||||||
|
if ('_id' in docValue && parentIssue !== undefined && parentIssue?._id !== docValue.attachedTo) {
|
||||||
|
let rank: string | null = null
|
||||||
|
|
||||||
if (parentIssue) {
|
if (parentIssue) {
|
||||||
const lastAttachedIssue = await client.findOne<Issue>(
|
const lastAttachedIssue = await client.findOne<Issue>(
|
||||||
tracker.class.Issue,
|
tracker.class.Issue,
|
||||||
{ attachedTo: parentIssue._id },
|
{ attachedTo: parentIssue._id },
|
||||||
{ sort: { rank: SortingOrder.Descending } }
|
{ sort: { rank: SortingOrder.Descending } }
|
||||||
)
|
)
|
||||||
|
|
||||||
rank = calcRank(lastAttachedIssue, undefined)
|
rank = calcRank(lastAttachedIssue, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.update(docValue, {
|
||||||
|
attachedTo: parentIssue === null ? tracker.ids.NoParent : parentIssue._id,
|
||||||
|
...(rank ? { rank } : {})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.update(value, {
|
|
||||||
attachedTo: parentIssue === null ? tracker.ids.NoParent : parentIssue._id,
|
|
||||||
...(rank ? { rank } : {})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch('close', parentIssue)
|
dispatch('close', parentIssue)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selected = 'attachedTo' in value ? value.attachedTo : undefined
|
$: selected = !Array.isArray(value) ? ('attachedTo' in value ? value.attachedTo : undefined) : undefined
|
||||||
$: ignoreObjects = '_id' in value ? [value._id] : []
|
$: ignoreObjects = !Array.isArray(value) ? ('_id' in value ? [value._id] : []) : undefined
|
||||||
$: updateIssueStatusCategories()
|
$: updateIssueStatusCategories()
|
||||||
$: 'space' in value && spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([team] = res))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ObjectPopup
|
<ObjectPopup
|
||||||
@ -79,14 +81,14 @@
|
|||||||
create={undefined}
|
create={undefined}
|
||||||
{ignoreObjects}
|
{ignoreObjects}
|
||||||
shadows={true}
|
shadows={true}
|
||||||
width="large"
|
{width}
|
||||||
searchField="title"
|
searchField="title"
|
||||||
on:update
|
on:update
|
||||||
on:close={onClose}
|
on:close={onClose}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="item" let:item={issue}>
|
<svelte:fragment slot="item" let:item={issue}>
|
||||||
{@const { icon } = statusCategoryById?.get(issue.$lookup?.status.category) ?? {}}
|
{@const { icon } = statusCategoryById?.get(issue.$lookup?.status.category) ?? {}}
|
||||||
{@const issueId = team && getIssueId(team, issue)}
|
{@const issueId = getIssueId(issue.$lookup.space, issue)}
|
||||||
{#if issueId && icon}
|
{#if issueId && icon}
|
||||||
<div class="flex-center clear-mins w-full h-9">
|
<div class="flex-center clear-mins w-full h-9">
|
||||||
<div class="icon mr-4 h-8">
|
<div class="icon mr-4 h-8">
|
||||||
|
@ -195,7 +195,7 @@
|
|||||||
{#each groupedIssues[category] as docObject (docObject._id)}
|
{#each groupedIssues[category] as docObject (docObject._id)}
|
||||||
<div
|
<div
|
||||||
bind:this={objectRefs[combinedGroupedIssues.findIndex((x) => x === docObject)]}
|
bind:this={objectRefs[combinedGroupedIssues.findIndex((x) => x === docObject)]}
|
||||||
class="listGrid antiList__row row gap-2"
|
class="listGrid antiList__row row gap-2 flex-grow"
|
||||||
class:checking={selectedObjectIdsSet.has(docObject._id)}
|
class:checking={selectedObjectIdsSet.has(docObject._id)}
|
||||||
class:mListGridFixed={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
|
class:mListGridFixed={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
|
||||||
class:mListGridSelected={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
|
class:mListGridSelected={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
|
||||||
@ -254,7 +254,6 @@
|
|||||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||||
{...attributeModel.props}
|
{...attributeModel.props}
|
||||||
/>
|
/>
|
||||||
<div class="grow-cell" />
|
|
||||||
{:else if attributeModel.props?.fixed}
|
{:else if attributeModel.props?.fixed}
|
||||||
<FixedColumn
|
<FixedColumn
|
||||||
width={propsWidth[attributeModel.key]}
|
width={propsWidth[attributeModel.key]}
|
||||||
|
@ -67,19 +67,19 @@
|
|||||||
{#if kind === 'list'}
|
{#if kind === 'list'}
|
||||||
<div class="priority-container" on:click={handlePriorityEditorOpened}>
|
<div class="priority-container" on:click={handlePriorityEditorOpened}>
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
{#if issuePriorities[value.priority].icon}<Icon icon={issuePriorities[value.priority].icon} {size} />{/if}
|
{#if issuePriorities[value.priority]?.icon}<Icon icon={issuePriorities[value.priority]?.icon} {size} />{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if shouldShowLabel}
|
{#if shouldShowLabel}
|
||||||
<span class="overflow-label label">
|
<span class="overflow-label label">
|
||||||
<Label label={issuePriorities[value.priority].label} />
|
<Label label={issuePriorities[value.priority]?.label} />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
showTooltip={isEditable ? { label: tracker.string.SetPriority } : undefined}
|
showTooltip={isEditable ? { label: tracker.string.SetPriority } : undefined}
|
||||||
label={shouldShowLabel ? issuePriorities[value.priority].label : undefined}
|
label={shouldShowLabel ? issuePriorities[value.priority]?.label : undefined}
|
||||||
icon={issuePriorities[value.priority].icon}
|
icon={issuePriorities[value.priority]?.icon}
|
||||||
{justify}
|
{justify}
|
||||||
{width}
|
{width}
|
||||||
{size}
|
{size}
|
||||||
|
@ -13,13 +13,20 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { WithLookup } from '@anticrm/core'
|
||||||
import { IssueStatus } from '@anticrm/tracker'
|
import { IssueStatus } from '@anticrm/tracker'
|
||||||
|
import { Icon } from '@anticrm/ui'
|
||||||
|
|
||||||
export let value: IssueStatus | undefined
|
export let value: WithLookup<IssueStatus> | undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if value}
|
{#if value}
|
||||||
<span class="overflow-label">
|
<div class="flex-presenter">
|
||||||
{value.name}
|
{#if value.$lookup?.category?.icon}
|
||||||
</span>
|
<Icon icon={value.$lookup?.category?.icon} size={'medium'} />
|
||||||
|
{/if}
|
||||||
|
<span class="overflow-label" class:ml-2={value.$lookup?.category?.icon !== undefined}>
|
||||||
|
{value.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if value}
|
{#if value}
|
||||||
<span class="titleLabel" class:mTitleLabelWithMargin={shouldUseMargin} title={value.title}>{value.title}</span>
|
<span class="titleLabel flex-grow" class:mTitleLabelWithMargin={shouldUseMargin} title={value.title}
|
||||||
|
>{value.title}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -57,6 +57,7 @@ import Views from './components/views/Views.svelte'
|
|||||||
import KanbanView from './components/issues/KanbanView.svelte'
|
import KanbanView from './components/issues/KanbanView.svelte'
|
||||||
import tracker from './plugin'
|
import tracker from './plugin'
|
||||||
import { getIssueId, getIssueTitle } from './utils'
|
import { getIssueId, getIssueTitle } from './utils'
|
||||||
|
import CreateIssue from './components/CreateIssue.svelte'
|
||||||
|
|
||||||
export async function queryIssue<D extends Issue> (
|
export async function queryIssue<D extends Issue> (
|
||||||
_class: Ref<Class<D>>,
|
_class: Ref<Class<D>>,
|
||||||
@ -144,7 +145,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
KanbanView,
|
KanbanView,
|
||||||
TeamProjects,
|
TeamProjects,
|
||||||
Roadmap,
|
Roadmap,
|
||||||
IssuePreview
|
IssuePreview,
|
||||||
|
CreateIssue
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
IssueQuery: async (client: Client, query: string) => await queryIssue(tracker.class.Issue, client, query)
|
IssueQuery: async (client: Client, query: string) => await queryIssue(tracker.class.Issue, client, query)
|
||||||
|
@ -57,6 +57,7 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
CreateTeam: '' as IntlString,
|
CreateTeam: '' as IntlString,
|
||||||
AddIssue: '' as IntlString,
|
AddIssue: '' as IntlString,
|
||||||
NewIssue: '' as IntlString,
|
NewIssue: '' as IntlString,
|
||||||
|
NewSubIssue: '' as IntlString,
|
||||||
Team: '' as IntlString,
|
Team: '' as IntlString,
|
||||||
SelectIssue: '' as IntlString,
|
SelectIssue: '' as IntlString,
|
||||||
SelectTeam: '' as IntlString,
|
SelectTeam: '' as IntlString,
|
||||||
@ -209,7 +210,8 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
KanbanView: '' as AnyComponent,
|
KanbanView: '' as AnyComponent,
|
||||||
Roadmap: '' as AnyComponent,
|
Roadmap: '' as AnyComponent,
|
||||||
TeamProjects: '' as AnyComponent,
|
TeamProjects: '' as AnyComponent,
|
||||||
IssuePreview: '' as AnyComponent
|
IssuePreview: '' as AnyComponent,
|
||||||
|
CreateIssue: '' as AnyComponent
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
getIssueTitle: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>
|
getIssueTitle: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>
|
||||||
|
77
plugins/tracker-resources/src/types.ts
Normal file
77
plugins/tracker-resources/src/types.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { Asset, IntlString } from '@anticrm/platform'
|
||||||
|
import {
|
||||||
|
IssuePriority,
|
||||||
|
IssuesDateModificationPeriod,
|
||||||
|
IssuesGrouping,
|
||||||
|
IssuesOrdering,
|
||||||
|
ProjectStatus
|
||||||
|
} from '@anticrm/tracker'
|
||||||
|
import tracker from './plugin'
|
||||||
|
|
||||||
|
export const issuePriorities: Record<IssuePriority, { icon: Asset, label: IntlString }> = {
|
||||||
|
[IssuePriority.NoPriority]: { icon: tracker.icon.PriorityNoPriority, label: tracker.string.NoPriority },
|
||||||
|
[IssuePriority.Urgent]: { icon: tracker.icon.PriorityUrgent, label: tracker.string.Urgent },
|
||||||
|
[IssuePriority.High]: { icon: tracker.icon.PriorityHigh, label: tracker.string.High },
|
||||||
|
[IssuePriority.Medium]: { icon: tracker.icon.PriorityMedium, label: tracker.string.Medium },
|
||||||
|
[IssuePriority.Low]: { icon: tracker.icon.PriorityLow, label: tracker.string.Low }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const issuesGroupByOptions: Record<IssuesGrouping, IntlString> = {
|
||||||
|
[IssuesGrouping.Status]: tracker.string.Status,
|
||||||
|
[IssuesGrouping.Assignee]: tracker.string.Assignee,
|
||||||
|
[IssuesGrouping.Priority]: tracker.string.Priority,
|
||||||
|
[IssuesGrouping.Project]: tracker.string.Project,
|
||||||
|
[IssuesGrouping.NoGrouping]: tracker.string.NoGrouping
|
||||||
|
}
|
||||||
|
|
||||||
|
export const issuesOrderByOptions: Record<IssuesOrdering, IntlString> = {
|
||||||
|
[IssuesOrdering.Status]: tracker.string.Status,
|
||||||
|
[IssuesOrdering.Priority]: tracker.string.Priority,
|
||||||
|
[IssuesOrdering.LastUpdated]: tracker.string.LastUpdated,
|
||||||
|
[IssuesOrdering.DueDate]: tracker.string.DueDate
|
||||||
|
}
|
||||||
|
|
||||||
|
export const issuesDateModificationPeriodOptions: Record<IssuesDateModificationPeriod, IntlString> = {
|
||||||
|
[IssuesDateModificationPeriod.All]: tracker.string.All,
|
||||||
|
[IssuesDateModificationPeriod.PastWeek]: tracker.string.PastWeek,
|
||||||
|
[IssuesDateModificationPeriod.PastMonth]: tracker.string.PastMonth
|
||||||
|
}
|
||||||
|
export const defaultProjectStatuses = [
|
||||||
|
ProjectStatus.Backlog,
|
||||||
|
ProjectStatus.Planned,
|
||||||
|
ProjectStatus.InProgress,
|
||||||
|
ProjectStatus.Paused,
|
||||||
|
ProjectStatus.Completed,
|
||||||
|
ProjectStatus.Canceled
|
||||||
|
]
|
||||||
|
|
||||||
|
export const projectStatusAssets: Record<ProjectStatus, { icon: Asset, label: IntlString }> = {
|
||||||
|
[ProjectStatus.Backlog]: { icon: tracker.icon.ProjectStatusBacklog, label: tracker.string.Backlog },
|
||||||
|
[ProjectStatus.Planned]: { icon: tracker.icon.ProjectStatusPlanned, label: tracker.string.Planned },
|
||||||
|
[ProjectStatus.InProgress]: { icon: tracker.icon.ProjectStatusInProgress, label: tracker.string.InProgress },
|
||||||
|
[ProjectStatus.Paused]: { icon: tracker.icon.ProjectStatusPaused, label: tracker.string.Paused },
|
||||||
|
[ProjectStatus.Completed]: { icon: tracker.icon.ProjectStatusCompleted, label: tracker.string.Completed },
|
||||||
|
[ProjectStatus.Canceled]: { icon: tracker.icon.ProjectStatusCanceled, label: tracker.string.Canceled }
|
||||||
|
}
|
||||||
|
export const defaultPriorities = [
|
||||||
|
IssuePriority.NoPriority,
|
||||||
|
IssuePriority.Urgent,
|
||||||
|
IssuePriority.High,
|
||||||
|
IssuePriority.Medium,
|
||||||
|
IssuePriority.Low
|
||||||
|
]
|
@ -15,20 +15,22 @@
|
|||||||
|
|
||||||
import contact, { Employee, formatName } from '@anticrm/contact'
|
import contact, { Employee, formatName } from '@anticrm/contact'
|
||||||
import { Doc, DocumentQuery, Ref, SortingOrder, TxOperations } from '@anticrm/core'
|
import { Doc, DocumentQuery, Ref, SortingOrder, TxOperations } from '@anticrm/core'
|
||||||
|
import { TypeState } from '@anticrm/kanban'
|
||||||
import { Asset, IntlString, translate } from '@anticrm/platform'
|
import { Asset, IntlString, translate } from '@anticrm/platform'
|
||||||
import {
|
import {
|
||||||
IssuePriority,
|
|
||||||
Team,
|
|
||||||
IssuesGrouping,
|
|
||||||
IssuesOrdering,
|
|
||||||
Issue,
|
Issue,
|
||||||
IssuesDateModificationPeriod,
|
IssuesDateModificationPeriod,
|
||||||
|
IssuesGrouping,
|
||||||
|
IssuesOrdering,
|
||||||
|
IssueStatus,
|
||||||
ProjectStatus,
|
ProjectStatus,
|
||||||
IssueStatus
|
Team
|
||||||
} from '@anticrm/tracker'
|
} from '@anticrm/tracker'
|
||||||
import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
|
import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
|
||||||
import { TypeState } from '@anticrm/kanban'
|
|
||||||
import tracker from './plugin'
|
import tracker from './plugin'
|
||||||
|
import { defaultPriorities, defaultProjectStatuses, issuePriorities } from './types'
|
||||||
|
|
||||||
|
export * from './types'
|
||||||
|
|
||||||
export interface NavigationItem {
|
export interface NavigationItem {
|
||||||
id: string
|
id: string
|
||||||
@ -44,35 +46,6 @@ export interface Selection {
|
|||||||
currentSpecial?: string
|
currentSpecial?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const issuePriorities: Record<IssuePriority, { icon: Asset, label: IntlString }> = {
|
|
||||||
[IssuePriority.NoPriority]: { icon: tracker.icon.PriorityNoPriority, label: tracker.string.NoPriority },
|
|
||||||
[IssuePriority.Urgent]: { icon: tracker.icon.PriorityUrgent, label: tracker.string.Urgent },
|
|
||||||
[IssuePriority.High]: { icon: tracker.icon.PriorityHigh, label: tracker.string.High },
|
|
||||||
[IssuePriority.Medium]: { icon: tracker.icon.PriorityMedium, label: tracker.string.Medium },
|
|
||||||
[IssuePriority.Low]: { icon: tracker.icon.PriorityLow, label: tracker.string.Low }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const issuesGroupByOptions: Record<IssuesGrouping, IntlString> = {
|
|
||||||
[IssuesGrouping.Status]: tracker.string.Status,
|
|
||||||
[IssuesGrouping.Assignee]: tracker.string.Assignee,
|
|
||||||
[IssuesGrouping.Priority]: tracker.string.Priority,
|
|
||||||
[IssuesGrouping.Project]: tracker.string.Project,
|
|
||||||
[IssuesGrouping.NoGrouping]: tracker.string.NoGrouping
|
|
||||||
}
|
|
||||||
|
|
||||||
export const issuesOrderByOptions: Record<IssuesOrdering, IntlString> = {
|
|
||||||
[IssuesOrdering.Status]: tracker.string.Status,
|
|
||||||
[IssuesOrdering.Priority]: tracker.string.Priority,
|
|
||||||
[IssuesOrdering.LastUpdated]: tracker.string.LastUpdated,
|
|
||||||
[IssuesOrdering.DueDate]: tracker.string.DueDate
|
|
||||||
}
|
|
||||||
|
|
||||||
export const issuesDateModificationPeriodOptions: Record<IssuesDateModificationPeriod, IntlString> = {
|
|
||||||
[IssuesDateModificationPeriod.All]: tracker.string.All,
|
|
||||||
[IssuesDateModificationPeriod.PastWeek]: tracker.string.PastWeek,
|
|
||||||
[IssuesDateModificationPeriod.PastMonth]: tracker.string.PastMonth
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' | 'project'>
|
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' | 'project'>
|
||||||
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate'>
|
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate'>
|
||||||
|
|
||||||
@ -120,24 +93,6 @@ export const getIssuesModificationDatePeriodTime = (period: IssuesDateModificati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultProjectStatuses = [
|
|
||||||
ProjectStatus.Backlog,
|
|
||||||
ProjectStatus.Planned,
|
|
||||||
ProjectStatus.InProgress,
|
|
||||||
ProjectStatus.Paused,
|
|
||||||
ProjectStatus.Completed,
|
|
||||||
ProjectStatus.Canceled
|
|
||||||
]
|
|
||||||
|
|
||||||
export const projectStatusAssets: Record<ProjectStatus, { icon: Asset, label: IntlString }> = {
|
|
||||||
[ProjectStatus.Backlog]: { icon: tracker.icon.ProjectStatusBacklog, label: tracker.string.Backlog },
|
|
||||||
[ProjectStatus.Planned]: { icon: tracker.icon.ProjectStatusPlanned, label: tracker.string.Planned },
|
|
||||||
[ProjectStatus.InProgress]: { icon: tracker.icon.ProjectStatusInProgress, label: tracker.string.InProgress },
|
|
||||||
[ProjectStatus.Paused]: { icon: tracker.icon.ProjectStatusPaused, label: tracker.string.Paused },
|
|
||||||
[ProjectStatus.Completed]: { icon: tracker.icon.ProjectStatusCompleted, label: tracker.string.Completed },
|
|
||||||
[ProjectStatus.Canceled]: { icon: tracker.icon.ProjectStatusCanceled, label: tracker.string.Canceled }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const groupBy = (data: any, key: any): { [key: string]: any[] } => {
|
export const groupBy = (data: any, key: any): { [key: string]: any[] } => {
|
||||||
return data.reduce((storage: { [key: string]: any[] }, item: any) => {
|
return data.reduce((storage: { [key: string]: any[] }, item: any) => {
|
||||||
const group = item[key]
|
const group = item[key]
|
||||||
@ -224,14 +179,6 @@ export const getIssueFilterAssetsByType = (type: string): { icon: Asset, label:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPriorities = [
|
|
||||||
IssuePriority.NoPriority,
|
|
||||||
IssuePriority.Urgent,
|
|
||||||
IssuePriority.High,
|
|
||||||
IssuePriority.Medium,
|
|
||||||
IssuePriority.Low
|
|
||||||
]
|
|
||||||
|
|
||||||
export const getArraysIntersection = (a: any[], b: any[]): any[] => {
|
export const getArraysIntersection = (a: any[], b: any[]): any[] => {
|
||||||
const setB = new Set(b)
|
const setB = new Set(b)
|
||||||
const intersection = new Set(a.filter((x) => setB.has(x)))
|
const intersection = new Set(a.filter((x) => setB.has(x)))
|
||||||
|
@ -267,7 +267,11 @@ export default plugin(trackerId, {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
SetDueDate: '' as Ref<Action>,
|
SetDueDate: '' as Ref<Action>,
|
||||||
SetParent: '' as Ref<Action>
|
SetParent: '' as Ref<Action>,
|
||||||
|
SetStatus: '' as Ref<Action>,
|
||||||
|
SetPriority: '' as Ref<Action>,
|
||||||
|
SetAssignee: '' as Ref<Action>,
|
||||||
|
SetProject: '' as Ref<Action>
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
DefaultTeam: '' as Ref<Team>
|
DefaultTeam: '' as Ref<Team>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Doc, Hierarchy } from '@anticrm/core'
|
import { Class, Doc, DocumentQuery, Hierarchy, Ref } from '@anticrm/core'
|
||||||
import { getResource, Resource } from '@anticrm/platform'
|
import { Asset, getResource, IntlString, Resource } from '@anticrm/platform'
|
||||||
import { getClient, MessageBox, updateAttribute } from '@anticrm/presentation'
|
import { getClient, MessageBox, updateAttribute } from '@anticrm/presentation'
|
||||||
import {
|
import {
|
||||||
AnyComponent,
|
AnyComponent,
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
showPanel,
|
showPanel,
|
||||||
showPopup
|
showPopup
|
||||||
} from '@anticrm/ui'
|
} from '@anticrm/ui'
|
||||||
import { ViewContext } from '@anticrm/view'
|
import { Action, ViewContext } from '@anticrm/view'
|
||||||
import MoveView from './components/Move.svelte'
|
import MoveView from './components/Move.svelte'
|
||||||
import { contextStore } from './context'
|
import { contextStore } from './context'
|
||||||
import view from './plugin'
|
import view from './plugin'
|
||||||
@ -168,6 +168,7 @@ async function ShowPopup (
|
|||||||
value?: string
|
value?: string
|
||||||
values?: string
|
values?: string
|
||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
|
fillProps?: Record<string, string>
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const docs = Array.isArray(doc) ? doc : doc !== undefined ? [doc] : []
|
const docs = Array.isArray(doc) ? doc : doc !== undefined ? [doc] : []
|
||||||
@ -176,6 +177,19 @@ async function ShowPopup (
|
|||||||
let cprops = {
|
let cprops = {
|
||||||
...(props?.props ?? {})
|
...(props?.props ?? {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [docKey, propKey] of Object.entries(props.fillProps ?? {})) {
|
||||||
|
for (const dv of docs) {
|
||||||
|
const dvv = (dv as any)[docKey]
|
||||||
|
if (dvv !== undefined) {
|
||||||
|
;(cprops as any)[propKey] = { dvv }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (docKey === '_object') {
|
||||||
|
;(cprops as any)[propKey] = docs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (docs.length > 0) {
|
if (docs.length > 0) {
|
||||||
cprops = {
|
cprops = {
|
||||||
...cprops,
|
...cprops,
|
||||||
@ -282,6 +296,36 @@ function UpdateDocument (doc: Doc | Doc[], evt: Event, props: Record<string, any
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ValueSelector (
|
||||||
|
doc: Doc | Doc[],
|
||||||
|
evt: Event,
|
||||||
|
props: {
|
||||||
|
action: Action
|
||||||
|
|
||||||
|
attribute: string
|
||||||
|
|
||||||
|
// Class object finder
|
||||||
|
_class?: Ref<Class<Doc>>
|
||||||
|
query?: DocumentQuery<Doc>
|
||||||
|
// Will copy values from selection document to query
|
||||||
|
// If set of docs passed, will do $in for values.
|
||||||
|
fillQuery?: Record<string, string>
|
||||||
|
|
||||||
|
// A list of fields with matched values to perform action.
|
||||||
|
docMatches?: string[]
|
||||||
|
searchField?: string
|
||||||
|
|
||||||
|
// Or list of values to select from
|
||||||
|
values?: Array<{ icon?: Asset, label: IntlString, id: number | string }>
|
||||||
|
|
||||||
|
placeholder?: IntlString
|
||||||
|
}
|
||||||
|
): void {
|
||||||
|
if (props.action.actionPopup !== undefined) {
|
||||||
|
showPopup(props.action.actionPopup, { ...props, ...props.action.actionProps, value: doc, width: 'large' }, 'top')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getPopupAlignment (
|
async function getPopupAlignment (
|
||||||
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>,
|
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>,
|
||||||
evt?: Event
|
evt?: Event
|
||||||
@ -319,5 +363,6 @@ export const actionImpl = {
|
|||||||
UpdateDocument,
|
UpdateDocument,
|
||||||
ShowPanel,
|
ShowPanel,
|
||||||
ShowPopup,
|
ShowPopup,
|
||||||
ShowEditor
|
ShowEditor,
|
||||||
|
ValueSelector
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
import { WithLookup } from '@anticrm/core'
|
import { WithLookup } from '@anticrm/core'
|
||||||
import { getResource, translate } from '@anticrm/platform'
|
import { getResource, translate } from '@anticrm/platform'
|
||||||
import { createQuery, getClient } from '@anticrm/presentation'
|
import { createQuery, getClient } from '@anticrm/presentation'
|
||||||
import { closePopup, Icon, IconArrowLeft, Label } from '@anticrm/ui'
|
import ui, { Button, closePopup, Component, Icon, IconArrowLeft, Label } from '@anticrm/ui'
|
||||||
import { Action, ViewContext } from '@anticrm/view'
|
import { Action, ViewContext } from '@anticrm/view'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { filterActions, getSelection } from '../actions'
|
import { filterActions, getSelection } from '../actions'
|
||||||
@ -25,18 +25,22 @@
|
|||||||
import ActionContext from './ActionContext.svelte'
|
import ActionContext from './ActionContext.svelte'
|
||||||
import { ListView } from '@anticrm/ui'
|
import { ListView } from '@anticrm/ui'
|
||||||
import ObjectPresenter from './ObjectPresenter.svelte'
|
import ObjectPresenter from './ObjectPresenter.svelte'
|
||||||
|
import { tick } from 'svelte'
|
||||||
|
|
||||||
export let viewContext: ViewContext
|
export let viewContext: ViewContext
|
||||||
|
|
||||||
let search: string = ''
|
let search: string = ''
|
||||||
let actions: WithLookup<Action>[] = []
|
let actions: WithLookup<Action>[] = []
|
||||||
let input: HTMLInputElement
|
let input: HTMLInputElement | undefined
|
||||||
|
|
||||||
const query = createQuery()
|
const query = createQuery()
|
||||||
|
|
||||||
query.query(
|
query.query(
|
||||||
view.class.Action,
|
view.class.Action,
|
||||||
{},
|
{
|
||||||
|
// Disable popup actions for now
|
||||||
|
// actionPopup: { $exists: false }
|
||||||
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
actions = res
|
actions = res
|
||||||
},
|
},
|
||||||
@ -47,12 +51,6 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const targetQuery = createQuery()
|
|
||||||
|
|
||||||
targetQuery.query(view.class.Action, {}, (res) => {
|
|
||||||
actions = res
|
|
||||||
})
|
|
||||||
|
|
||||||
let supportedActions: WithLookup<Action>[] = []
|
let supportedActions: WithLookup<Action>[] = []
|
||||||
let filteredActions: WithLookup<Action>[] = []
|
let filteredActions: WithLookup<Action>[] = []
|
||||||
|
|
||||||
@ -107,21 +105,27 @@
|
|||||||
let list: ListView
|
let list: ListView
|
||||||
/* eslint-disable no-undef */
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
let activeAction: Action | undefined
|
||||||
|
|
||||||
async function handleSelection (evt: Event, selection: number): Promise<void> {
|
async function handleSelection (evt: Event, selection: number): Promise<void> {
|
||||||
const action = filteredActions[selection]
|
const action = filteredActions[selection]
|
||||||
|
if (action.actionPopup !== undefined) {
|
||||||
|
activeAction = action
|
||||||
|
return
|
||||||
|
}
|
||||||
const docs = getSelection($focusStore, $selectionStore)
|
const docs = getSelection($focusStore, $selectionStore)
|
||||||
if (action.input === 'focus') {
|
if (action.input === 'focus') {
|
||||||
const impl = await getResource(action.action)
|
const impl = await getResource(action.action)
|
||||||
if (impl !== undefined) {
|
if (impl !== undefined) {
|
||||||
closePopup()
|
closePopup()
|
||||||
impl(docs[0], evt, action.actionProps)
|
impl(docs[0], evt, { ...action.actionProps, action })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (action.input === 'selection' || action.input === 'any' || action.input === 'none') {
|
if (action.input === 'selection' || action.input === 'any' || action.input === 'none') {
|
||||||
const impl = await getResource(action.action)
|
const impl = await getResource(action.action)
|
||||||
if (impl !== undefined) {
|
if (impl !== undefined) {
|
||||||
closePopup()
|
closePopup()
|
||||||
impl(docs, evt, action.actionProps)
|
impl(docs, evt, { ...action.actionProps, action })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,7 +180,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="selectPopup width-40" style:width="15rem" on:keydown={onKeydown}>
|
<div class="selectPopup width-40" style:width="15rem" on:keydown={onKeydown}>
|
||||||
<div class="mt-2 ml-2">
|
<div class="mt-2 ml-2 flex-between">
|
||||||
{#if $selectionStore.length > 0}
|
{#if $selectionStore.length > 0}
|
||||||
<div class="item-box">
|
<div class="item-box">
|
||||||
{$selectionStore.length} items
|
{$selectionStore.length} items
|
||||||
@ -191,64 +195,93 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if activeAction && activeAction?.actionPopup !== undefined}
|
||||||
|
<div class="mt-2 mb-2 mr-2">
|
||||||
|
<Button
|
||||||
|
icon={IconArrowLeft}
|
||||||
|
label={ui.string.Back}
|
||||||
|
on:click={() => {
|
||||||
|
activeAction = undefined
|
||||||
|
}}
|
||||||
|
width={'fit-content'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="header">
|
{#if activeAction && activeAction?.actionPopup !== undefined}
|
||||||
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} />
|
<Component
|
||||||
</div>
|
is={activeAction?.actionPopup}
|
||||||
<div class="scroll">
|
props={{
|
||||||
<div class="box">
|
...activeAction.actionProps,
|
||||||
<ListView
|
value: getSelection($focusStore, $selectionStore),
|
||||||
bind:this={list}
|
width: 'full',
|
||||||
count={filteredActions.length}
|
size: 'medium'
|
||||||
bind:selection
|
}}
|
||||||
on:click={(evt) => handleSelection(evt, evt.detail)}
|
on:close={async () => {
|
||||||
>
|
activeAction = undefined
|
||||||
<svelte:fragment slot="category" let:item>
|
await tick()
|
||||||
{@const action = filteredActions[item]}
|
input?.focus()
|
||||||
{#if item === 0 || (item > 0 && filteredActions[item - 1].$lookup?.category?.label !== action.$lookup?.category?.label)}
|
}}
|
||||||
<!--Category for first item-->
|
/>
|
||||||
{#if action.$lookup?.category}
|
{:else}
|
||||||
<div class="category-box">
|
<div class="header">
|
||||||
<Label label={action.$lookup.category.label} />
|
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} />
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</svelte:fragment>
|
|
||||||
<svelte:fragment slot="item" let:item>
|
|
||||||
{@const action = filteredActions[item]}
|
|
||||||
<div class="flex-row-center flex-between flex-grow ml-2 p-3 text-base">
|
|
||||||
<div class="mr-4">
|
|
||||||
<Icon icon={action.icon ?? IconArrowLeft} size={'small'} />
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow">
|
|
||||||
<Label label={action.label} />
|
|
||||||
</div>
|
|
||||||
<div class="mr-2 text-md flex-row-center">
|
|
||||||
{#if action.keyBinding}
|
|
||||||
{#each action.keyBinding as key, i}
|
|
||||||
{#if i !== 0}
|
|
||||||
<div class="ml-2 mr-2">or</div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex-row-center">
|
|
||||||
{#each formatKey(key) as k, jj}
|
|
||||||
{#if jj !== 0}
|
|
||||||
<div class="ml-1 mr-1">then</div>
|
|
||||||
{/if}
|
|
||||||
{#each k as kk, j}
|
|
||||||
<div class="flex-center text-sm key-box">
|
|
||||||
{kk}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</svelte:fragment>
|
|
||||||
</ListView>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="scroll">
|
||||||
|
<div class="box">
|
||||||
|
<ListView
|
||||||
|
bind:this={list}
|
||||||
|
count={filteredActions.length}
|
||||||
|
bind:selection
|
||||||
|
on:click={(evt) => handleSelection(evt, evt.detail)}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="category" let:item>
|
||||||
|
{@const action = filteredActions[item]}
|
||||||
|
{#if item === 0 || (item > 0 && filteredActions[item - 1].$lookup?.category?.label !== action.$lookup?.category?.label)}
|
||||||
|
<!--Category for first item-->
|
||||||
|
{#if action.$lookup?.category}
|
||||||
|
<div class="category-box">
|
||||||
|
<Label label={action.$lookup.category.label} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="item" let:item>
|
||||||
|
{@const action = filteredActions[item]}
|
||||||
|
<div class="flex-row-center flex-between flex-grow ml-2 p-3 text-base">
|
||||||
|
<div class="mr-4">
|
||||||
|
<Icon icon={action.icon ?? IconArrowLeft} size={'small'} />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<Label label={action.label} />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2 text-md flex-row-center">
|
||||||
|
{#if action.keyBinding}
|
||||||
|
{#each action.keyBinding as key, i}
|
||||||
|
{#if i !== 0}
|
||||||
|
<div class="ml-2 mr-2">or</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-row-center">
|
||||||
|
{#each formatKey(key) as k, jj}
|
||||||
|
{#if jj !== 0}
|
||||||
|
<div class="ml-1 mr-1">then</div>
|
||||||
|
{/if}
|
||||||
|
{#each k as kk, j}
|
||||||
|
<div class="flex-center text-sm key-box">
|
||||||
|
{kk}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ListView>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
import type { Asset } from '@anticrm/platform'
|
import type { Asset } from '@anticrm/platform'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
import { Action, Menu } from '@anticrm/ui'
|
import { Action, Menu } from '@anticrm/ui'
|
||||||
import type { ViewContextType } from '@anticrm/view'
|
import type { ActionGroup, ViewContextType } from '@anticrm/view'
|
||||||
import { getActions, invokeAction } from '../actions'
|
import { getActions, invokeAction } from '../actions'
|
||||||
|
|
||||||
export let object: Doc | Doc[]
|
export let object: Doc | Doc[]
|
||||||
@ -31,15 +31,29 @@
|
|||||||
|
|
||||||
let loaded = 0
|
let loaded = 0
|
||||||
|
|
||||||
|
const order: Record<ActionGroup, number> = {
|
||||||
|
create: 1,
|
||||||
|
edit: 2,
|
||||||
|
copy: 3,
|
||||||
|
associate: 4,
|
||||||
|
tools: 5,
|
||||||
|
other: 6
|
||||||
|
}
|
||||||
|
|
||||||
getActions(client, object, baseMenuClass, mode).then((result) => {
|
getActions(client, object, baseMenuClass, mode).then((result) => {
|
||||||
actions = result.map((a) => ({
|
actions = result
|
||||||
label: a.label,
|
.sort((a, b) => order[a.context.group ?? 'other'] - order[b.context.group ?? 'other'])
|
||||||
icon: a.icon as Asset,
|
.map((a) => ({
|
||||||
inline: a.inline,
|
label: a.label,
|
||||||
action: async (_: any, evt: Event) => {
|
icon: a.icon as Asset,
|
||||||
invokeAction(object, evt, a.action, a.actionProps)
|
inline: a.inline,
|
||||||
}
|
group: a.context.group ?? 'other',
|
||||||
}))
|
action: async (_: any, evt: Event) => {
|
||||||
|
invokeAction(object, evt, a.action, a.actionProps)
|
||||||
|
},
|
||||||
|
component: a.actionPopup,
|
||||||
|
props: { ...a.actionProps, value: object }
|
||||||
|
}))
|
||||||
loaded = 1
|
loaded = 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
142
plugins/view-resources/src/components/ValueSelector.svelte
Normal file
142
plugins/view-resources/src/components/ValueSelector.svelte
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||||
|
import { Asset, IntlString } from '@anticrm/platform'
|
||||||
|
import { getClient, ObjectPopup } from '@anticrm/presentation'
|
||||||
|
import { Label, SelectPopup } from '@anticrm/ui'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import view from '../plugin'
|
||||||
|
import ObjectPresenter from './ObjectPresenter.svelte'
|
||||||
|
|
||||||
|
export let value: Doc | Doc[]
|
||||||
|
export let isEditable: boolean = true
|
||||||
|
|
||||||
|
export let _class: Ref<Class<Doc>> | undefined
|
||||||
|
export let query: DocumentQuery<Doc> | undefined
|
||||||
|
export let queryOptions: FindOptions<Doc> | undefined
|
||||||
|
|
||||||
|
export let attribute: string
|
||||||
|
export let searchField: string
|
||||||
|
export let values:
|
||||||
|
| {
|
||||||
|
icon?: Asset
|
||||||
|
label: IntlString
|
||||||
|
id: string | number
|
||||||
|
}[]
|
||||||
|
| undefined = undefined
|
||||||
|
|
||||||
|
export let fillQuery: Record<string, string> | undefined
|
||||||
|
export let docMatches: string[] | undefined
|
||||||
|
export let placeholder: IntlString | undefined
|
||||||
|
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||||
|
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const changeStatus = async (newStatus: any) => {
|
||||||
|
if (!isEditable || newStatus == null) {
|
||||||
|
dispatch('close', null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const docs = Array.isArray(value) ? value : [value]
|
||||||
|
const c = getClient()
|
||||||
|
|
||||||
|
const changed = (d: Doc) => (d as any)[attribute] !== newStatus
|
||||||
|
await Promise.all(docs.filter(changed).map((it) => c.update(it, { [attribute]: newStatus })))
|
||||||
|
|
||||||
|
dispatch('close', newStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: current = (value as any)[attribute]
|
||||||
|
|
||||||
|
let finalQuery: DocumentQuery<Doc> = {}
|
||||||
|
|
||||||
|
let docMatch = true
|
||||||
|
|
||||||
|
function updateQuery (
|
||||||
|
query: DocumentQuery<Doc> | undefined,
|
||||||
|
value: Doc | Doc[],
|
||||||
|
fillQuery: Record<string, string> | undefined
|
||||||
|
): void {
|
||||||
|
// Check if docMatches is applied.
|
||||||
|
|
||||||
|
if (docMatches !== undefined && Array.isArray(value)) {
|
||||||
|
for (const k of docMatches) {
|
||||||
|
const v = (value[0] as any)[k]
|
||||||
|
for (const d of value) {
|
||||||
|
if (v !== (d as any)[k]) {
|
||||||
|
docMatch = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = { ...query }
|
||||||
|
const docs = Array.isArray(value) ? value : [value]
|
||||||
|
for (const [docKey, queryKey] of Object.entries(fillQuery ?? {})) {
|
||||||
|
const vs: any[] = []
|
||||||
|
|
||||||
|
for (const dv of docs) {
|
||||||
|
const dvv = (dv as any)[docKey]
|
||||||
|
if (dvv !== undefined) {
|
||||||
|
if (!vs.includes(dvv)) {
|
||||||
|
vs.push(dvv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;(q as any)[queryKey] = docs.length === 1 ? vs[0] : { $in: vs }
|
||||||
|
|
||||||
|
if (docKey === '_object') {
|
||||||
|
;(q as any)[queryKey] = docs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalQuery = q
|
||||||
|
docMatch = true
|
||||||
|
}
|
||||||
|
|
||||||
|
$: updateQuery(query, value, fillQuery)
|
||||||
|
$: huge = size === 'medium' || size === 'large'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if docMatch}
|
||||||
|
{#if values}
|
||||||
|
<SelectPopup
|
||||||
|
value={values.map((it) => ({ ...it, isSelected: it.id === current }))}
|
||||||
|
on:close={(evt) => changeStatus(evt.detail)}
|
||||||
|
placeholder={placeholder ?? view.string.Filter}
|
||||||
|
searchable
|
||||||
|
{width}
|
||||||
|
{size}
|
||||||
|
/>
|
||||||
|
{:else if _class !== undefined}
|
||||||
|
<ObjectPopup
|
||||||
|
{_class}
|
||||||
|
docQuery={finalQuery}
|
||||||
|
options={queryOptions ?? {}}
|
||||||
|
{searchField}
|
||||||
|
allowDeselect={true}
|
||||||
|
selected={current}
|
||||||
|
on:close={(evt) => changeStatus(evt.detail?._id)}
|
||||||
|
placeholder={placeholder ?? view.string.Filter}
|
||||||
|
{width}
|
||||||
|
{size}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="item" let:item>
|
||||||
|
<div class="flex flex-grow overflow-label" class:mt-2={huge} class:mb-2={huge}>
|
||||||
|
<ObjectPresenter
|
||||||
|
objectId={item._id}
|
||||||
|
_class={item._class}
|
||||||
|
value={item}
|
||||||
|
props={{ isInteractive: false, inline: true, size }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ObjectPopup>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="selectPopup">
|
||||||
|
<div class="flex-center w-60 h-18">
|
||||||
|
<Label label={view.string.DontMatchCriteria} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -51,6 +51,7 @@ import TableBrowser from './components/TableBrowser.svelte'
|
|||||||
import TimestampPresenter from './components/TimestampPresenter.svelte'
|
import TimestampPresenter from './components/TimestampPresenter.svelte'
|
||||||
import UpDownNavigator from './components/UpDownNavigator.svelte'
|
import UpDownNavigator from './components/UpDownNavigator.svelte'
|
||||||
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||||
|
import ValueSelector from './components/ValueSelector.svelte'
|
||||||
import {
|
import {
|
||||||
afterResult,
|
afterResult,
|
||||||
beforeResult,
|
beforeResult,
|
||||||
@ -136,7 +137,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
StringEditorPopup: EditBoxPopup,
|
StringEditorPopup: EditBoxPopup,
|
||||||
BooleanTruePresenter,
|
BooleanTruePresenter,
|
||||||
EnumEditor,
|
EnumEditor,
|
||||||
FilterTypePopup
|
FilterTypePopup,
|
||||||
|
ValueSelector
|
||||||
},
|
},
|
||||||
popup: {
|
popup: {
|
||||||
PositionElementAlignment
|
PositionElementAlignment
|
||||||
|
@ -110,6 +110,11 @@ export class ListSelectionProvider implements SelectionFocusProvider {
|
|||||||
update (docs: Doc[]): void {
|
update (docs: Doc[]): void {
|
||||||
this._docs = docs
|
this._docs = docs
|
||||||
|
|
||||||
|
selectionStore.update((docs) => {
|
||||||
|
const ids = new Set(docs.map((it) => it._id))
|
||||||
|
return this._docs.filter((it) => ids.has(it._id))
|
||||||
|
})
|
||||||
|
|
||||||
if (this._docs.length > 0) {
|
if (this._docs.length > 0) {
|
||||||
if (this._current?.focus === undefined) {
|
if (this._current?.focus === undefined) {
|
||||||
this.delegate(0, undefined, 'vertical')
|
this.delegate(0, undefined, 'vertical')
|
||||||
|
@ -215,6 +215,11 @@ export interface ActionCategory extends Doc, UXObject {
|
|||||||
visible: boolean
|
visible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type ActionGroup = 'create' | 'edit' | 'associate' | 'copy' | 'tools' | 'other'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -224,6 +229,9 @@ export interface Action<T extends Doc = Doc, P = Record<string, any>> extends Do
|
|||||||
// Action implementation parameters
|
// Action implementation parameters
|
||||||
actionProps?: P
|
actionProps?: P
|
||||||
|
|
||||||
|
// If specified, will show sub menu based on actionPopup/actionProps
|
||||||
|
actionPopup?: AnyComponent
|
||||||
|
|
||||||
// If specified, action could be used only with one item selected.
|
// If specified, action could be used only with one item selected.
|
||||||
// single - one object is required
|
// single - one object is required
|
||||||
// any - one or multiple objects are required
|
// any - one or multiple objects are required
|
||||||
@ -275,8 +283,9 @@ export interface ViewContext {
|
|||||||
mode: ViewContextType | ViewContextType[]
|
mode: ViewContextType | ViewContextType[]
|
||||||
// Active application
|
// Active application
|
||||||
application?: Ref<Doc>
|
application?: Ref<Doc>
|
||||||
|
|
||||||
// Optional groupping
|
// Optional groupping
|
||||||
group?: string
|
group?: ActionGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -435,7 +444,8 @@ const view = plugin(viewId, {
|
|||||||
ObjectPresenter: '' as AnyComponent,
|
ObjectPresenter: '' as AnyComponent,
|
||||||
EditDoc: '' as AnyComponent,
|
EditDoc: '' as AnyComponent,
|
||||||
SpacePresenter: '' as AnyComponent,
|
SpacePresenter: '' as AnyComponent,
|
||||||
BooleanTruePresenter: '' as AnyComponent
|
BooleanTruePresenter: '' as AnyComponent,
|
||||||
|
ValueSelector: '' as AnyComponent
|
||||||
},
|
},
|
||||||
string: {
|
string: {
|
||||||
CustomizeView: '' as IntlString,
|
CustomizeView: '' as IntlString,
|
||||||
@ -499,11 +509,33 @@ const view = plugin(viewId, {
|
|||||||
value?: string
|
value?: string
|
||||||
values?: string
|
values?: string
|
||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
|
// Will copy values from selection document to props
|
||||||
|
fillProps?: Record<string, string>
|
||||||
}>,
|
}>,
|
||||||
ShowEditor: '' as ViewAction<{
|
ShowEditor: '' as ViewAction<{
|
||||||
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>
|
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>
|
||||||
attribute: string
|
attribute: string
|
||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
|
}>,
|
||||||
|
ValueSelector: '' as ViewAction<{
|
||||||
|
attribute: string
|
||||||
|
|
||||||
|
// Class object finder
|
||||||
|
_class?: Ref<Class<Doc>>
|
||||||
|
query?: DocumentQuery<Doc>
|
||||||
|
queryOptions?: FindOptions<Doc>
|
||||||
|
// Will copy values from selection document to query
|
||||||
|
// If set of docs passed, will do $in for values.
|
||||||
|
fillQuery?: Record<string, string>
|
||||||
|
|
||||||
|
// A list of fields with matched values to perform action.
|
||||||
|
docMatches?: string[]
|
||||||
|
searchField?: string
|
||||||
|
|
||||||
|
// Or list of values to select from
|
||||||
|
values?: { icon?: Asset, label: IntlString, id: number | string }[]
|
||||||
|
|
||||||
|
placeholder?: IntlString
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user