mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
parent
82c8d70d37
commit
d935e9b963
11
changelog.md
11
changelog.md
@ -2,15 +2,23 @@
|
||||
|
||||
## 0.6.29 (upcoming)
|
||||
|
||||
Platform:
|
||||
|
||||
- Object selector actions
|
||||
|
||||
Tracker:
|
||||
|
||||
- Remember view options
|
||||
- Remember view options
|
||||
- My issues
|
||||
- Roadmap
|
||||
- Remember view options
|
||||
- Context menus (Priority/Status/Assignee)
|
||||
|
||||
Chunter:
|
||||
|
||||
- Reactions on messages
|
||||
- Priority filter
|
||||
- Context menu selector for state/assignee
|
||||
|
||||
HR:
|
||||
|
||||
@ -27,7 +35,6 @@ Tracker:
|
||||
- Issue state history.
|
||||
- Subissue issue popup.
|
||||
- Label support
|
||||
- Priority filter
|
||||
|
||||
Lead:
|
||||
|
||||
|
@ -114,7 +114,8 @@ export function createModel (builder: Builder): void {
|
||||
category: attachment.category.Attachments,
|
||||
target: attachment.class.Attachment,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'edit'
|
||||
}
|
||||
})
|
||||
|
||||
@ -133,7 +134,8 @@ export function createModel (builder: Builder): void {
|
||||
category: attachment.category.Attachments,
|
||||
target: attachment.class.Attachment,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'edit'
|
||||
}
|
||||
})
|
||||
|
||||
@ -152,7 +154,8 @@ export function createModel (builder: Builder): void {
|
||||
category: attachment.category.Attachments,
|
||||
target: attachment.class.Attachment,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'edit'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -283,7 +283,7 @@ export function createModel (builder: Builder): void {
|
||||
input: 'any',
|
||||
category: board.category.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
|
||||
)
|
||||
@ -294,7 +294,9 @@ export function createModel (builder: Builder): void {
|
||||
actionProps: {
|
||||
component: tags.component.TagsEditorPopup,
|
||||
element: view.popup.PositionElementAlignment,
|
||||
value: 'object'
|
||||
fillProps: {
|
||||
_object: 'value'
|
||||
}
|
||||
},
|
||||
label: board.string.Labels,
|
||||
icon: tags.icon.Tags,
|
||||
@ -302,7 +304,7 @@ export function createModel (builder: Builder): void {
|
||||
inline: true,
|
||||
category: board.category.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
|
||||
)
|
||||
@ -320,7 +322,7 @@ export function createModel (builder: Builder): void {
|
||||
inline: true,
|
||||
category: board.category.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
|
||||
)
|
||||
@ -331,7 +333,9 @@ export function createModel (builder: Builder): void {
|
||||
actionProps: {
|
||||
component: board.component.CoverActionPopup,
|
||||
element: view.popup.PositionElementAlignment,
|
||||
value: 'object'
|
||||
fillProps: {
|
||||
_object: 'value'
|
||||
}
|
||||
},
|
||||
label: board.string.Cover,
|
||||
icon: board.icon.Card,
|
||||
@ -339,7 +343,7 @@ export function createModel (builder: Builder): void {
|
||||
inline: true,
|
||||
category: board.category.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
|
||||
)
|
||||
|
@ -176,7 +176,8 @@ export function createModel (builder: Builder): void {
|
||||
category: calendar.category.Calendar,
|
||||
target: calendar.class.Event,
|
||||
context: {
|
||||
mode: 'context'
|
||||
mode: 'context',
|
||||
group: 'create'
|
||||
}
|
||||
},
|
||||
calendar.action.SaveEventReminder
|
||||
|
@ -238,7 +238,8 @@ export function createModel (builder: Builder): void {
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.Message,
|
||||
context: {
|
||||
mode: 'context'
|
||||
mode: 'context',
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
chunter.action.MarkUnread
|
||||
@ -253,7 +254,8 @@ export function createModel (builder: Builder): void {
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.ThreadMessage,
|
||||
context: {
|
||||
mode: 'context'
|
||||
mode: 'context',
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
chunter.action.MarkCommentUnread
|
||||
@ -272,7 +274,8 @@ export function createModel (builder: Builder): void {
|
||||
archived: false
|
||||
},
|
||||
context: {
|
||||
mode: 'context'
|
||||
mode: 'context',
|
||||
group: 'tools'
|
||||
}
|
||||
},
|
||||
chunter.action.ArchiveChannel
|
||||
@ -291,7 +294,8 @@ export function createModel (builder: Builder): void {
|
||||
archived: true
|
||||
},
|
||||
context: {
|
||||
mode: 'context'
|
||||
mode: 'context',
|
||||
group: 'tools'
|
||||
}
|
||||
},
|
||||
chunter.action.UnarchiveChannel
|
||||
@ -307,7 +311,8 @@ export function createModel (builder: Builder): void {
|
||||
category: chunter.category.Chunter,
|
||||
target: chunter.class.DirectMessage,
|
||||
context: {
|
||||
mode: 'context'
|
||||
mode: 'context',
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
chunter.action.ConvertToPrivate
|
||||
|
@ -388,7 +388,10 @@ export function createModel (builder: Builder): void {
|
||||
createAction(builder, {
|
||||
...actionTemplates.open,
|
||||
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',
|
||||
category: hr.category.HR,
|
||||
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
|
||||
)
|
||||
@ -289,7 +289,7 @@ export function createModel (builder: Builder): void {
|
||||
_id: { $nin: [hr.ids.Head] }
|
||||
},
|
||||
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
|
||||
)
|
||||
@ -305,7 +305,7 @@ export function createModel (builder: Builder): void {
|
||||
input: 'any',
|
||||
category: hr.category.HR,
|
||||
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
|
||||
)
|
||||
|
@ -160,7 +160,8 @@ export function createModel (builder: Builder): void {
|
||||
category: inventory.category.Inventory,
|
||||
target: inventory.class.Category,
|
||||
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 contact, { TContact } from '@anticrm/model-contact'
|
||||
import core from '@anticrm/model-core'
|
||||
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
|
||||
import view, { createAction } from '@anticrm/model-view'
|
||||
import task, { actionTemplates, TSpaceWithStates, TTask } from '@anticrm/model-task'
|
||||
import view, { createAction, actionTemplates as viewTemplates } from '@anticrm/model-view'
|
||||
import workbench, { Application } from '@anticrm/model-workbench'
|
||||
import setting from '@anticrm/setting'
|
||||
import lead from './plugin'
|
||||
@ -67,6 +67,8 @@ export class TCustomer extends TContact implements Customer {
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
const archiveId = 'archive'
|
||||
|
||||
builder.createModel(TFunnel, TLead, TCustomer)
|
||||
|
||||
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
|
||||
},
|
||||
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: [
|
||||
@ -129,6 +140,22 @@ export function createModel (builder: Builder): void {
|
||||
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(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
@ -248,6 +275,9 @@ export function createModel (builder: Builder): void {
|
||||
element: 'top',
|
||||
props: {
|
||||
preserveCustomer: true
|
||||
},
|
||||
fillProps: {
|
||||
_id: 'customer'
|
||||
}
|
||||
},
|
||||
label: lead.string.CreateLead,
|
||||
@ -255,7 +285,10 @@ export function createModel (builder: Builder): void {
|
||||
input: 'focus',
|
||||
category: lead.category.Lead,
|
||||
target: contact.class.Contact,
|
||||
context: { mode: ['context', 'browser'] },
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'associate'
|
||||
},
|
||||
override: [lead.action.CreateGlobalLead]
|
||||
})
|
||||
|
||||
@ -275,7 +308,8 @@ export function createModel (builder: Builder): void {
|
||||
target: core.class.Doc,
|
||||
context: {
|
||||
mode: ['workbench', 'browser'],
|
||||
application: lead.app.Lead
|
||||
application: lead.app.Lead,
|
||||
group: 'create'
|
||||
}
|
||||
},
|
||||
lead.action.CreateGlobalLead
|
||||
|
@ -394,10 +394,12 @@ export function createModel (builder: Builder): void {
|
||||
action: view.actionImpl.ShowPopup,
|
||||
actionProps: {
|
||||
component: recruit.component.CreateApplication,
|
||||
_id: 'candidate',
|
||||
element: 'top',
|
||||
props: {
|
||||
preserveCandidate: true
|
||||
},
|
||||
fillProps: {
|
||||
_id: 'candidate'
|
||||
}
|
||||
},
|
||||
label: recruit.string.CreateAnApplication,
|
||||
@ -405,7 +407,10 @@ export function createModel (builder: Builder): void {
|
||||
input: 'focus',
|
||||
category: recruit.category.Recruit,
|
||||
target: contact.class.Person,
|
||||
context: { mode: ['context', 'browser'] },
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'associate'
|
||||
},
|
||||
override: [recruit.action.CreateGlobalApplication]
|
||||
})
|
||||
createAction(builder, {
|
||||
@ -422,7 +427,8 @@ export function createModel (builder: Builder): void {
|
||||
target: core.class.Doc,
|
||||
context: {
|
||||
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,
|
||||
context: {
|
||||
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,
|
||||
context: {
|
||||
mode: ['workbench', 'browser'],
|
||||
application: recruit.app.Recruit
|
||||
application: recruit.app.Recruit,
|
||||
group: 'create'
|
||||
}
|
||||
},
|
||||
recruit.action.CreateGlobalApplication
|
||||
@ -504,7 +512,8 @@ export function createModel (builder: Builder): void {
|
||||
keyBinding: ['e'],
|
||||
target: recruit.class.Vacancy,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'create'
|
||||
}
|
||||
})
|
||||
|
||||
@ -526,12 +535,13 @@ export function createModel (builder: Builder): void {
|
||||
|
||||
createReviewModel(builder)
|
||||
|
||||
// createAction(builder, { ...viewTemplates.open, target: recruit.class.Vacancy, context: { mode: ['browser', 'context'] } })
|
||||
|
||||
createAction(builder, {
|
||||
...viewTemplates.open,
|
||||
target: recruit.class.Vacancy,
|
||||
context: { mode: ['browser', 'context'] },
|
||||
context: {
|
||||
mode: ['browser', 'context'],
|
||||
group: 'create'
|
||||
},
|
||||
action: workbench.actionImpl.Navigate,
|
||||
actionProps: {
|
||||
mode: 'space'
|
||||
@ -541,7 +551,10 @@ export function createModel (builder: Builder): void {
|
||||
createAction(builder, {
|
||||
...viewTemplates.open,
|
||||
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 {
|
||||
@ -574,6 +587,55 @@ export function createModel (builder: Builder): void {
|
||||
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'
|
||||
|
@ -49,7 +49,8 @@ export function createReviewModel (builder: Builder): void {
|
||||
category: recruit.category.Recruit,
|
||||
target: recruit.class.Review,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'create'
|
||||
}
|
||||
},
|
||||
recruit.action.CreateOpinion
|
||||
@ -75,11 +76,13 @@ export function createReviewModel (builder: Builder): void {
|
||||
action: view.actionImpl.ShowPopup,
|
||||
actionProps: {
|
||||
component: recruit.component.CreateReview,
|
||||
_id: 'candidate',
|
||||
_space: 'space',
|
||||
element: 'top',
|
||||
props: {
|
||||
preserveCandidate: true
|
||||
},
|
||||
fillProps: {
|
||||
space: '_space',
|
||||
_id: 'candidate'
|
||||
}
|
||||
},
|
||||
label: recruit.string.CreateReview,
|
||||
@ -88,7 +91,8 @@ export function createReviewModel (builder: Builder): void {
|
||||
category: recruit.category.Recruit,
|
||||
target: recruit.mixin.Candidate,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'associate'
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -281,7 +281,8 @@ export const actionTemplates = template({
|
||||
archived: false
|
||||
},
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'tools'
|
||||
}
|
||||
},
|
||||
unarchiveSpace: {
|
||||
@ -301,7 +302,8 @@ export const actionTemplates = template({
|
||||
archived: true
|
||||
},
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'tools'
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -427,7 +429,8 @@ export function createModel (builder: Builder): void {
|
||||
archived: false
|
||||
},
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
task.action.EditStatuses
|
||||
@ -487,7 +490,8 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
target: task.class.TodoItem,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'edit'
|
||||
}
|
||||
})
|
||||
|
||||
@ -505,7 +509,8 @@ export function createModel (builder: Builder): void {
|
||||
done: true
|
||||
},
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'edit'
|
||||
},
|
||||
target: task.class.TodoItem
|
||||
})
|
||||
@ -516,7 +521,8 @@ export function createModel (builder: Builder): void {
|
||||
...viewTemplates.move,
|
||||
target: task.class.Task,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'tools'
|
||||
}
|
||||
},
|
||||
task.action.Move
|
||||
@ -542,7 +548,8 @@ export function createModel (builder: Builder): void {
|
||||
category: task.category.Task,
|
||||
target: task.class.State,
|
||||
context: {
|
||||
mode: ['context', 'browser']
|
||||
mode: ['context', 'browser'],
|
||||
group: 'tools'
|
||||
}
|
||||
},
|
||||
task.action.ArchiveState
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
import type { Employee } 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 {
|
||||
ArrOf,
|
||||
Builder,
|
||||
@ -55,6 +55,7 @@ import tags from '@anticrm/tags'
|
||||
import tracker from './plugin'
|
||||
|
||||
import presentation from '@anticrm/model-presentation'
|
||||
import { defaultPriorities, issuePriorities } from '@anticrm/tracker-resources/src/types'
|
||||
|
||||
export { trackerOperation } from './migration'
|
||||
export { default } from './plugin'
|
||||
@ -565,22 +566,25 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
action: view.actionImpl.ShowPopup,
|
||||
actionProps: {
|
||||
component: tracker.component.SetDueDateActionPopup,
|
||||
props: { mondayStart: true, withTime: false },
|
||||
element: 'top'
|
||||
component: tracker.component.CreateIssue,
|
||||
element: 'top',
|
||||
fillProps: {
|
||||
_object: 'parentIssue'
|
||||
}
|
||||
},
|
||||
label: tracker.string.SetDueDate,
|
||||
icon: tracker.icon.DueDate,
|
||||
label: tracker.string.NewSubIssue,
|
||||
icon: tracker.icon.Issue,
|
||||
keyBinding: [],
|
||||
input: 'none',
|
||||
input: 'focus',
|
||||
category: tracker.category.Tracker,
|
||||
target: tracker.class.Issue,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
application: tracker.app.Tracker
|
||||
application: tracker.app.Tracker,
|
||||
group: 'associate'
|
||||
}
|
||||
},
|
||||
tracker.action.SetDueDate
|
||||
tracker.action.SetParent
|
||||
)
|
||||
|
||||
createAction(
|
||||
@ -598,13 +602,35 @@ export function createModel (builder: Builder): void {
|
||||
category: tracker.category.Tracker,
|
||||
target: tracker.class.Issue,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
application: tracker.app.Tracker
|
||||
mode: ['context'],
|
||||
application: tracker.app.Tracker,
|
||||
group: 'associate'
|
||||
}
|
||||
},
|
||||
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, {
|
||||
filters: ['status', 'priority', 'assignee', 'project', 'dueDate', 'modifiedOn']
|
||||
})
|
||||
@ -619,4 +645,139 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
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 { trackerId } from '@anticrm/tracker'
|
||||
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 { Application } from '@anticrm/workbench'
|
||||
|
||||
|
@ -44,14 +44,13 @@
|
||||
},
|
||||
(result) => {
|
||||
objects = result
|
||||
dispatch('content', objects)
|
||||
},
|
||||
{
|
||||
...options
|
||||
}
|
||||
)
|
||||
|
||||
$: dispatch('content', objects)
|
||||
|
||||
function getStateObjects (
|
||||
objects: Item[],
|
||||
state: TypeState,
|
||||
|
@ -22,7 +22,9 @@
|
||||
createFocusManager,
|
||||
EditBox,
|
||||
FocusHandler,
|
||||
Icon,
|
||||
IconAdd,
|
||||
IconCheck,
|
||||
ListView,
|
||||
showPopup,
|
||||
Tooltip
|
||||
@ -44,7 +46,8 @@
|
||||
export let selectedObjects: Ref<Doc>[] = []
|
||||
export let ignoreObjects: Ref<Doc>[] = []
|
||||
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'
|
||||
|
||||
@ -56,7 +59,7 @@
|
||||
label: IntlString
|
||||
update: (doc: Doc) => string
|
||||
}
|
||||
| undefined
|
||||
| undefined = undefined
|
||||
|
||||
let search: string = ''
|
||||
let objects: Doc[] = []
|
||||
@ -162,7 +165,13 @@
|
||||
|
||||
<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">
|
||||
<EditBox kind={'search-style'} focusIndex={1} focus bind:value={search} {placeholder} />
|
||||
{#if create !== undefined}
|
||||
@ -170,7 +179,7 @@
|
||||
<Button
|
||||
focusIndex={2}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
{size}
|
||||
icon={IconAdd}
|
||||
showTooltip={{ label: create.label }}
|
||||
on:click={onCreate}
|
||||
@ -200,23 +209,27 @@
|
||||
handleSelection(undefined, objects, item)
|
||||
}}
|
||||
>
|
||||
{#if multiSelect}
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={selectedElements.has(obj._id)} primary />
|
||||
{#if allowDeselect && selected}
|
||||
<div class="icon">
|
||||
{#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>
|
||||
{/if}
|
||||
|
||||
<slot name="item" item={obj} />
|
||||
|
||||
{#if allowDeselect && obj._id === selected}
|
||||
<div class="check-right pointer-events-none">
|
||||
{#if titleDeselect}
|
||||
<Tooltip label={titleDeselect ?? presentation.string.Deselect}>
|
||||
<CheckBox checked circle primary />
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<CheckBox checked circle primary />
|
||||
{/if}
|
||||
<span class="label">
|
||||
<slot name="item" item={obj} />
|
||||
</span>
|
||||
{#if multiSelect}
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={selectedElements.has(obj._id)} primary />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
@ -25,6 +25,15 @@
|
||||
background: var(--popup-bg-color);
|
||||
border-radius: .5rem;
|
||||
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; }
|
||||
&.autoHeight {
|
||||
@ -242,11 +251,11 @@
|
||||
margin: 0;
|
||||
|
||||
.icon { color: var(--content-color); }
|
||||
&:focus .icon { color: var(--accent-color); }
|
||||
&.hover .icon { color: var(--accent-color); }
|
||||
}
|
||||
|
||||
// &:hover { background-color: var(--popup-bg-hover); }
|
||||
&:focus {
|
||||
&.hover {
|
||||
background-color: var(--popup-bg-hover);
|
||||
outline: none;
|
||||
// box-shadow: inset 0 0 1px 1px var(--primary-edit-border-color);
|
||||
@ -258,6 +267,12 @@
|
||||
&:active { 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 {
|
||||
margin-left: 1rem;
|
||||
@ -360,7 +375,9 @@
|
||||
.selectPopup .menu-item.arrow,
|
||||
.antiPopup-submenu {
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
&::after {
|
||||
content: '▶';
|
||||
position: absolute;
|
||||
|
@ -38,6 +38,7 @@
|
||||
"DaysAfter": "{days, plural, =1 {in a day} other {in # days}}",
|
||||
"NoActionsDefined": "No actions applicable",
|
||||
"Incoming": "Incoming",
|
||||
"HoursLabel": "Hours"
|
||||
"HoursLabel": "Hours",
|
||||
"Back": "Back"
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@
|
||||
"DaysAfter": "{days, plural, =1 {через день} other {через # дней}}",
|
||||
"NoActionsDefined": "Нет доступных действий",
|
||||
"Incoming": "Входящие",
|
||||
"HoursLabel": "Часы"
|
||||
"HoursLabel": "Часы",
|
||||
"Back": "Назад"
|
||||
}
|
||||
}
|
||||
|
@ -13,76 +13,148 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
|
||||
import { afterUpdate, createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import ui from '../plugin'
|
||||
import { closePopup, showPopup } from '../popups'
|
||||
import { Action } from '../types'
|
||||
import Icon from './Icon.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import MouseSpeedTracker from './MouseSpeedTracker.svelte'
|
||||
|
||||
export let actions: Action[] = []
|
||||
export let ctx: any = undefined
|
||||
|
||||
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') {
|
||||
dispatch('close')
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
}
|
||||
const n = btns.indexOf(activeElement) ?? 0
|
||||
if (ev.key === 'ArrowDown') {
|
||||
if (n === btns.length - 1) btns[0].focus()
|
||||
else btns[n + 1].focus()
|
||||
if (n < btns.length - 1) {
|
||||
activeElement = btns[n + 1]
|
||||
}
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
}
|
||||
if (ev.key === 'ArrowUp') {
|
||||
if (n === 0) btns[btns.length - 1].focus()
|
||||
else btns[n - 1].focus()
|
||||
if (n > 0) {
|
||||
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(() => {
|
||||
dispatch('update', Date.now())
|
||||
})
|
||||
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>
|
||||
|
||||
<div class="antiPopup">
|
||||
<div class="antiPopup" on:keydown={keyDown}>
|
||||
<MouseSpeedTracker bind:focusSpeed />
|
||||
<div class="ap-space" />
|
||||
<div class="ap-scroll">
|
||||
<div class="ap-box">
|
||||
<div class="ap-box" bind:this={popup}>
|
||||
{#if actions.length === 0}
|
||||
<div class="p-6 error-color">
|
||||
<Label label={ui.string.NoActionsDefined} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each actions as action, i}
|
||||
{#if i > 0 && actions[i - 1].group !== action.group}
|
||||
<span class="ap-menuItem separator" />
|
||||
{/if}
|
||||
{#if action.link}
|
||||
<a class="stealth" href={action.link}>
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<button
|
||||
bind:this={btns[i]}
|
||||
class="ap-menuItem flex-row-center withIcon w-full"
|
||||
on:keydown={(evt) => keyDown(evt, i)}
|
||||
on:mouseover={(evt) => evt.currentTarget.focus()}
|
||||
class:hover={btns[i] === activeElement}
|
||||
on:mouseover={(evt) => focusTarget(action, btns[i])}
|
||||
on:click|preventDefault|stopPropagation={(evt) => {
|
||||
if (!action.inline) dispatch('close')
|
||||
action.action(ctx, evt)
|
||||
}}
|
||||
>
|
||||
{#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>
|
||||
</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}
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<button
|
||||
bind:this={btns[i]}
|
||||
class="ap-menuItem flex-row-center withIcon"
|
||||
on:keydown={(evt) => keyDown(evt, i)}
|
||||
on:mouseover={(evt) => evt.currentTarget.focus()}
|
||||
class:hover={btns[i] === activeElement}
|
||||
on:mouseover={() => focusTarget(action, btns[i])}
|
||||
on:click={(evt) => {
|
||||
if (!action.inline) dispatch('close')
|
||||
action.action(ctx, evt)
|
||||
@ -91,7 +163,7 @@
|
||||
{#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>
|
||||
{/if}
|
||||
{/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}
|
||||
top={$modal.length - 1 === i}
|
||||
close={popup.close}
|
||||
overlay={popup.options.overlay}
|
||||
/>
|
||||
{/each}
|
||||
|
@ -23,6 +23,7 @@
|
||||
export let element: PopupAlignment | undefined
|
||||
export let onClose: ((result: any) => void) | undefined
|
||||
export let onUpdate: ((result: any) => void) | undefined
|
||||
export let overlay: boolean
|
||||
export let zIndex: number
|
||||
export let top: boolean
|
||||
export let close: () => void
|
||||
@ -126,12 +127,16 @@
|
||||
on:changeContent={fitPopup}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="modal-overlay"
|
||||
class:antiOverlay={options.showOverlay}
|
||||
style={`z-index: ${zIndex};`}
|
||||
on:click={() => escapeClose()}
|
||||
/>
|
||||
|
||||
{#if overlay}
|
||||
<div
|
||||
class="modal-overlay"
|
||||
class:antiOverlay={options.showOverlay}
|
||||
style={`z-index: ${zIndex};`}
|
||||
on:click={() => escapeClose()}
|
||||
on:keydown|stopPropagation|preventDefault={() => {}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.popup {
|
||||
|
@ -14,64 +14,124 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
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 IconCheck from './icons/Check.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 placeholderParam: any | undefined = undefined
|
||||
export let searchable: boolean = false
|
||||
export let value: Array<{ id: number | string; icon: Asset; label?: IntlString; text?: string; isSelected?: boolean }>
|
||||
export let width: 'medium' | 'large' = 'medium'
|
||||
export let value: Array<ValueType>
|
||||
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||
|
||||
let search: string = ''
|
||||
|
||||
let phTraslate: string = ''
|
||||
$: if (placeholder) {
|
||||
translate(placeholder, placeholderParam ?? {}).then((res) => {
|
||||
phTraslate = res
|
||||
})
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: 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>
|
||||
|
||||
<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}
|
||||
<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>
|
||||
{/if}
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each value.filter((el) => (el.label ?? el.text ?? '').toLowerCase().includes(search.toLowerCase())) as item}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => dispatch('close', item.id)}
|
||||
on:focus={() => dispatch('update', item)}
|
||||
on:mouseover={() => dispatch('update', item)}
|
||||
>
|
||||
{#if hasSelected}
|
||||
<div class="icon">
|
||||
{#if item.isSelected}
|
||||
<Icon icon={IconCheck} size={'small'} />
|
||||
<ListView bind:this={list} count={filteredObjects.length} bind:selection>
|
||||
<svelte:fragment slot="item" let:item={itemId}>
|
||||
{@const item = filteredObjects[itemId]}
|
||||
<button
|
||||
class="menu-item w-full"
|
||||
on:click={() => dispatch('close', item.id)}
|
||||
on:focus={() => dispatch('update', item)}
|
||||
on:mouseover={() => dispatch('update', item)}
|
||||
>
|
||||
<div class="flex-row-center" class:mt-2={huge} class:mb-2={huge}>
|
||||
{#if hasSelected}
|
||||
<div class="icon">
|
||||
{#if item.isSelected}
|
||||
<Icon icon={IconCheck} {size} />
|
||||
{/if}
|
||||
</div>
|
||||
{/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>
|
||||
{/if}
|
||||
<div class="icon"><Icon icon={item.icon} size={'small'} /></div>
|
||||
<span class="label">
|
||||
{#if item.label}
|
||||
<Label label={item.label} />
|
||||
{:else if item.text}
|
||||
<span>{item.text}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,7 +63,8 @@ export const uis = plugin(uiId, {
|
||||
DaysAfter: '' as IntlString,
|
||||
NoActionsDefined: '' as IntlString,
|
||||
Incoming: '' as IntlString,
|
||||
HoursLabel: '' as IntlString
|
||||
HoursLabel: '' as IntlString,
|
||||
Back: '' as IntlString
|
||||
},
|
||||
metadata: {
|
||||
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 { writable } from 'svelte/store'
|
||||
import type {
|
||||
AnyComponent,
|
||||
AnySvelteComponent,
|
||||
HorizontalAlignment,
|
||||
PopupAlignment,
|
||||
PopupOptions,
|
||||
PopupPositionElement,
|
||||
VerticalAlignment
|
||||
} from './types'
|
||||
|
||||
interface CompAndProps {
|
||||
id: string
|
||||
@ -18,9 +18,14 @@ interface CompAndProps {
|
||||
onClose?: (result: any) => void
|
||||
onUpdate?: (result: any) => void
|
||||
close: () => void
|
||||
options: {
|
||||
category: string
|
||||
overlay: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const popupstore = writable<CompAndProps[]>([])
|
||||
|
||||
function addPopup (props: CompAndProps): void {
|
||||
popupstore.update((popups) => {
|
||||
popups.push(props)
|
||||
@ -33,7 +38,11 @@ export function showPopup (
|
||||
props: any,
|
||||
element?: PopupAlignment,
|
||||
onClose?: (result: any) => void,
|
||||
onUpdate?: (result: any) => void
|
||||
onUpdate?: (result: any) => void,
|
||||
options: {
|
||||
category: string
|
||||
overlay: boolean
|
||||
} = { category: 'popup', overlay: true }
|
||||
): () => void {
|
||||
const id = `${popupId++}`
|
||||
const closePopupOp = (): void => {
|
||||
@ -47,17 +56,23 @@ export function showPopup (
|
||||
}
|
||||
if (typeof component === 'string') {
|
||||
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))
|
||||
} else {
|
||||
addPopup({ id, is: component, props, element, onClose, onUpdate, close: closePopupOp })
|
||||
addPopup({ id, is: component, props, element, onClose, onUpdate, close: closePopupOp, options })
|
||||
}
|
||||
return closePopupOp
|
||||
}
|
||||
|
||||
export function closePopup (): void {
|
||||
export function closePopup (category?: string): void {
|
||||
popupstore.update((popups) => {
|
||||
popups.pop()
|
||||
if (category !== undefined) {
|
||||
popups = popups.filter((p) => p.options.category !== category)
|
||||
} else {
|
||||
popups.pop()
|
||||
}
|
||||
return popups
|
||||
})
|
||||
}
|
||||
@ -179,15 +194,6 @@ export function fitPopupPositionedElement (
|
||||
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
|
||||
*
|
||||
|
@ -45,6 +45,12 @@ export interface Action {
|
||||
action: (props: any, ev: Event) => Promise<void>
|
||||
inline?: boolean
|
||||
link?: string
|
||||
|
||||
// Submenu component
|
||||
component?: AnyComponent
|
||||
props?: Record<string, any>
|
||||
|
||||
group?: string
|
||||
}
|
||||
|
||||
export interface IPopupItem {
|
||||
|
@ -14,6 +14,8 @@
|
||||
export let shouldShowPlaceholder = false
|
||||
export let onEmployeeEdit: ((event: MouseEvent) => void) | undefined = undefined
|
||||
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
|
||||
|
||||
@ -35,15 +37,17 @@
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="inline-flex clear-mins">
|
||||
<div class="over-underline">
|
||||
<div class:over-underline={!inline}>
|
||||
<PersonPresenter
|
||||
{value}
|
||||
{tooltipLabels}
|
||||
onEdit={handlePersonEdit}
|
||||
onEdit={isInteractive ? handlePersonEdit : () => {}}
|
||||
{shouldShowAvatar}
|
||||
{shouldShowName}
|
||||
{avatarSize}
|
||||
{shouldShowPlaceholder}
|
||||
{isInteractive}
|
||||
{inline}
|
||||
/>
|
||||
</div>
|
||||
{#if value?.$lookup?.statuses?.length}
|
||||
|
@ -153,7 +153,8 @@
|
||||
"DurDays": "{days, plural, =0 {today} =1 {1 day} other {# days }}",
|
||||
"DurMonths": "{months, plural, =0 {this month} =1 {1 month} other {# months}}",
|
||||
"DurYears": "{years, plural, =0 {this year} =1 {a year} other {# years}}",
|
||||
"StatusHistory": "State History"
|
||||
"StatusHistory": "State History",
|
||||
"NewSubIssue": "Add sub-issue..."
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -153,7 +153,8 @@
|
||||
"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 {# месяцев}}",
|
||||
"DurYears": "{years, plural, =0 {меньше года} =1 {год} =2 {2 года} =3 {3 года} =4 {4 года} other {# лет}}",
|
||||
"StatusHistory": "История состояний"
|
||||
"StatusHistory": "История состояний",
|
||||
"NewSubIssue": "Добавить под-задачу..."
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@
|
||||
|
||||
let currentAssignee: Ref<Employee> | null = assignee
|
||||
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||
let parentIssue: Issue | undefined
|
||||
export let parentIssue: Issue | undefined
|
||||
let labels: TagReference[] = []
|
||||
|
||||
let objectId: Ref<Issue> = generateId()
|
||||
|
@ -14,24 +14,24 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachedData, FindOptions, SortingOrder } from '@anticrm/core'
|
||||
import { Issue, IssueStatusCategory, Team, calcRank } from '@anticrm/tracker'
|
||||
import { createQuery, getClient, ObjectPopup } from '@anticrm/presentation'
|
||||
import { getClient, ObjectPopup } from '@anticrm/presentation'
|
||||
import { calcRank, Issue, IssueStatusCategory } from '@anticrm/tracker'
|
||||
import { Icon } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../plugin'
|
||||
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 spaceQuery = createQuery()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const options: FindOptions<Issue> = {
|
||||
lookup: { status: tracker.class.IssueStatus },
|
||||
lookup: { status: tracker.class.IssueStatus, space: tracker.class.Team },
|
||||
sort: { modifiedOn: SortingOrder.Descending }
|
||||
}
|
||||
|
||||
let team: Team | undefined
|
||||
let statusCategoryById: Map<string, IssueStatusCategory> | undefined
|
||||
|
||||
async function updateIssueStatusCategories () {
|
||||
@ -41,32 +41,34 @@
|
||||
}
|
||||
|
||||
async function onClose ({ detail: parentIssue }: CustomEvent<Issue | undefined | null>) {
|
||||
if ('_id' in value && parentIssue !== undefined && parentIssue?._id !== value.attachedTo) {
|
||||
let rank: string | null = null
|
||||
const vv = Array.isArray(value) ? value : [value]
|
||||
for (const docValue of vv) {
|
||||
if ('_id' in docValue && parentIssue !== undefined && parentIssue?._id !== docValue.attachedTo) {
|
||||
let rank: string | null = null
|
||||
|
||||
if (parentIssue) {
|
||||
const lastAttachedIssue = await client.findOne<Issue>(
|
||||
tracker.class.Issue,
|
||||
{ attachedTo: parentIssue._id },
|
||||
{ sort: { rank: SortingOrder.Descending } }
|
||||
)
|
||||
if (parentIssue) {
|
||||
const lastAttachedIssue = await client.findOne<Issue>(
|
||||
tracker.class.Issue,
|
||||
{ attachedTo: parentIssue._id },
|
||||
{ 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)
|
||||
}
|
||||
|
||||
$: selected = 'attachedTo' in value ? value.attachedTo : undefined
|
||||
$: ignoreObjects = '_id' in value ? [value._id] : []
|
||||
$: selected = !Array.isArray(value) ? ('attachedTo' in value ? value.attachedTo : undefined) : undefined
|
||||
$: ignoreObjects = !Array.isArray(value) ? ('_id' in value ? [value._id] : []) : undefined
|
||||
$: updateIssueStatusCategories()
|
||||
$: 'space' in value && spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([team] = res))
|
||||
</script>
|
||||
|
||||
<ObjectPopup
|
||||
@ -79,14 +81,14 @@
|
||||
create={undefined}
|
||||
{ignoreObjects}
|
||||
shadows={true}
|
||||
width="large"
|
||||
{width}
|
||||
searchField="title"
|
||||
on:update
|
||||
on:close={onClose}
|
||||
>
|
||||
<svelte:fragment slot="item" let:item={issue}>
|
||||
{@const { icon } = statusCategoryById?.get(issue.$lookup?.status.category) ?? {}}
|
||||
{@const issueId = team && getIssueId(team, issue)}
|
||||
{@const issueId = getIssueId(issue.$lookup.space, issue)}
|
||||
{#if issueId && icon}
|
||||
<div class="flex-center clear-mins w-full h-9">
|
||||
<div class="icon mr-4 h-8">
|
||||
|
@ -195,7 +195,7 @@
|
||||
{#each groupedIssues[category] as docObject (docObject._id)}
|
||||
<div
|
||||
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:mListGridFixed={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
|
||||
class:mListGridSelected={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
|
||||
@ -254,7 +254,6 @@
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
<div class="grow-cell" />
|
||||
{:else if attributeModel.props?.fixed}
|
||||
<FixedColumn
|
||||
width={propsWidth[attributeModel.key]}
|
||||
|
@ -67,19 +67,19 @@
|
||||
{#if kind === 'list'}
|
||||
<div class="priority-container" on:click={handlePriorityEditorOpened}>
|
||||
<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>
|
||||
{#if shouldShowLabel}
|
||||
<span class="overflow-label label">
|
||||
<Label label={issuePriorities[value.priority].label} />
|
||||
<Label label={issuePriorities[value.priority]?.label} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Button
|
||||
showTooltip={isEditable ? { label: tracker.string.SetPriority } : undefined}
|
||||
label={shouldShowLabel ? issuePriorities[value.priority].label : undefined}
|
||||
icon={issuePriorities[value.priority].icon}
|
||||
label={shouldShowLabel ? issuePriorities[value.priority]?.label : undefined}
|
||||
icon={issuePriorities[value.priority]?.icon}
|
||||
{justify}
|
||||
{width}
|
||||
{size}
|
||||
|
@ -13,13 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@anticrm/core'
|
||||
import { IssueStatus } from '@anticrm/tracker'
|
||||
import { Icon } from '@anticrm/ui'
|
||||
|
||||
export let value: IssueStatus | undefined
|
||||
export let value: WithLookup<IssueStatus> | undefined
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<span class="overflow-label">
|
||||
{value.name}
|
||||
</span>
|
||||
<div class="flex-presenter">
|
||||
{#if value.$lookup?.category?.icon}
|
||||
<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}
|
||||
|
@ -20,7 +20,9 @@
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -57,6 +57,7 @@ import Views from './components/views/Views.svelte'
|
||||
import KanbanView from './components/issues/KanbanView.svelte'
|
||||
import tracker from './plugin'
|
||||
import { getIssueId, getIssueTitle } from './utils'
|
||||
import CreateIssue from './components/CreateIssue.svelte'
|
||||
|
||||
export async function queryIssue<D extends Issue> (
|
||||
_class: Ref<Class<D>>,
|
||||
@ -144,7 +145,8 @@ export default async (): Promise<Resources> => ({
|
||||
KanbanView,
|
||||
TeamProjects,
|
||||
Roadmap,
|
||||
IssuePreview
|
||||
IssuePreview,
|
||||
CreateIssue
|
||||
},
|
||||
completion: {
|
||||
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,
|
||||
AddIssue: '' as IntlString,
|
||||
NewIssue: '' as IntlString,
|
||||
NewSubIssue: '' as IntlString,
|
||||
Team: '' as IntlString,
|
||||
SelectIssue: '' as IntlString,
|
||||
SelectTeam: '' as IntlString,
|
||||
@ -209,7 +210,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
KanbanView: '' as AnyComponent,
|
||||
Roadmap: '' as AnyComponent,
|
||||
TeamProjects: '' as AnyComponent,
|
||||
IssuePreview: '' as AnyComponent
|
||||
IssuePreview: '' as AnyComponent,
|
||||
CreateIssue: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
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 { Doc, DocumentQuery, Ref, SortingOrder, TxOperations } from '@anticrm/core'
|
||||
import { TypeState } from '@anticrm/kanban'
|
||||
import { Asset, IntlString, translate } from '@anticrm/platform'
|
||||
import {
|
||||
IssuePriority,
|
||||
Team,
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
Issue,
|
||||
IssuesDateModificationPeriod,
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
IssueStatus,
|
||||
ProjectStatus,
|
||||
IssueStatus
|
||||
Team
|
||||
} from '@anticrm/tracker'
|
||||
import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
|
||||
import { TypeState } from '@anticrm/kanban'
|
||||
import tracker from './plugin'
|
||||
import { defaultPriorities, defaultProjectStatuses, issuePriorities } from './types'
|
||||
|
||||
export * from './types'
|
||||
|
||||
export interface NavigationItem {
|
||||
id: string
|
||||
@ -44,35 +46,6 @@ export interface Selection {
|
||||
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 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[] } => {
|
||||
return data.reduce((storage: { [key: string]: any[] }, item: any) => {
|
||||
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[] => {
|
||||
const setB = new Set(b)
|
||||
const intersection = new Set(a.filter((x) => setB.has(x)))
|
||||
|
@ -267,7 +267,11 @@ export default plugin(trackerId, {
|
||||
},
|
||||
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: {
|
||||
DefaultTeam: '' as Ref<Team>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Doc, Hierarchy } from '@anticrm/core'
|
||||
import { getResource, Resource } from '@anticrm/platform'
|
||||
import { Class, Doc, DocumentQuery, Hierarchy, Ref } from '@anticrm/core'
|
||||
import { Asset, getResource, IntlString, Resource } from '@anticrm/platform'
|
||||
import { getClient, MessageBox, updateAttribute } from '@anticrm/presentation'
|
||||
import {
|
||||
AnyComponent,
|
||||
@ -11,7 +11,7 @@ import {
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@anticrm/ui'
|
||||
import { ViewContext } from '@anticrm/view'
|
||||
import { Action, ViewContext } from '@anticrm/view'
|
||||
import MoveView from './components/Move.svelte'
|
||||
import { contextStore } from './context'
|
||||
import view from './plugin'
|
||||
@ -168,6 +168,7 @@ async function ShowPopup (
|
||||
value?: string
|
||||
values?: string
|
||||
props?: Record<string, any>
|
||||
fillProps?: Record<string, string>
|
||||
}
|
||||
): Promise<void> {
|
||||
const docs = Array.isArray(doc) ? doc : doc !== undefined ? [doc] : []
|
||||
@ -176,6 +177,19 @@ async function ShowPopup (
|
||||
let cprops = {
|
||||
...(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) {
|
||||
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 (
|
||||
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>,
|
||||
evt?: Event
|
||||
@ -319,5 +363,6 @@ export const actionImpl = {
|
||||
UpdateDocument,
|
||||
ShowPanel,
|
||||
ShowPopup,
|
||||
ShowEditor
|
||||
ShowEditor,
|
||||
ValueSelector
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { WithLookup } from '@anticrm/core'
|
||||
import { getResource, translate } from '@anticrm/platform'
|
||||
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 { onMount } from 'svelte'
|
||||
import { filterActions, getSelection } from '../actions'
|
||||
@ -25,18 +25,22 @@
|
||||
import ActionContext from './ActionContext.svelte'
|
||||
import { ListView } from '@anticrm/ui'
|
||||
import ObjectPresenter from './ObjectPresenter.svelte'
|
||||
import { tick } from 'svelte'
|
||||
|
||||
export let viewContext: ViewContext
|
||||
|
||||
let search: string = ''
|
||||
let actions: WithLookup<Action>[] = []
|
||||
let input: HTMLInputElement
|
||||
let input: HTMLInputElement | undefined
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
query.query(
|
||||
view.class.Action,
|
||||
{},
|
||||
{
|
||||
// Disable popup actions for now
|
||||
// actionPopup: { $exists: false }
|
||||
},
|
||||
(res) => {
|
||||
actions = res
|
||||
},
|
||||
@ -47,12 +51,6 @@
|
||||
}
|
||||
)
|
||||
|
||||
const targetQuery = createQuery()
|
||||
|
||||
targetQuery.query(view.class.Action, {}, (res) => {
|
||||
actions = res
|
||||
})
|
||||
|
||||
let supportedActions: WithLookup<Action>[] = []
|
||||
let filteredActions: WithLookup<Action>[] = []
|
||||
|
||||
@ -107,21 +105,27 @@
|
||||
let list: ListView
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
let activeAction: Action | undefined
|
||||
|
||||
async function handleSelection (evt: Event, selection: number): Promise<void> {
|
||||
const action = filteredActions[selection]
|
||||
if (action.actionPopup !== undefined) {
|
||||
activeAction = action
|
||||
return
|
||||
}
|
||||
const docs = getSelection($focusStore, $selectionStore)
|
||||
if (action.input === 'focus') {
|
||||
const impl = await getResource(action.action)
|
||||
if (impl !== undefined) {
|
||||
closePopup()
|
||||
impl(docs[0], evt, action.actionProps)
|
||||
impl(docs[0], evt, { ...action.actionProps, action })
|
||||
}
|
||||
}
|
||||
if (action.input === 'selection' || action.input === 'any' || action.input === 'none') {
|
||||
const impl = await getResource(action.action)
|
||||
if (impl !== undefined) {
|
||||
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="mt-2 ml-2">
|
||||
<div class="mt-2 ml-2 flex-between">
|
||||
{#if $selectionStore.length > 0}
|
||||
<div class="item-box">
|
||||
{$selectionStore.length} items
|
||||
@ -191,64 +195,93 @@
|
||||
/>
|
||||
</div>
|
||||
{/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 class="header">
|
||||
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} />
|
||||
</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>
|
||||
{#if activeAction && activeAction?.actionPopup !== undefined}
|
||||
<Component
|
||||
is={activeAction?.actionPopup}
|
||||
props={{
|
||||
...activeAction.actionProps,
|
||||
value: getSelection($focusStore, $selectionStore),
|
||||
width: 'full',
|
||||
size: 'medium'
|
||||
}}
|
||||
on:close={async () => {
|
||||
activeAction = undefined
|
||||
await tick()
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="header">
|
||||
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} />
|
||||
</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>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -18,7 +18,7 @@
|
||||
import type { Asset } from '@anticrm/platform'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Action, Menu } from '@anticrm/ui'
|
||||
import type { ViewContextType } from '@anticrm/view'
|
||||
import type { ActionGroup, ViewContextType } from '@anticrm/view'
|
||||
import { getActions, invokeAction } from '../actions'
|
||||
|
||||
export let object: Doc | Doc[]
|
||||
@ -31,15 +31,29 @@
|
||||
|
||||
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) => {
|
||||
actions = result.map((a) => ({
|
||||
label: a.label,
|
||||
icon: a.icon as Asset,
|
||||
inline: a.inline,
|
||||
action: async (_: any, evt: Event) => {
|
||||
invokeAction(object, evt, a.action, a.actionProps)
|
||||
}
|
||||
}))
|
||||
actions = result
|
||||
.sort((a, b) => order[a.context.group ?? 'other'] - order[b.context.group ?? 'other'])
|
||||
.map((a) => ({
|
||||
label: a.label,
|
||||
icon: a.icon as Asset,
|
||||
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
|
||||
})
|
||||
|
||||
|
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 UpDownNavigator from './components/UpDownNavigator.svelte'
|
||||
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||
import ValueSelector from './components/ValueSelector.svelte'
|
||||
import {
|
||||
afterResult,
|
||||
beforeResult,
|
||||
@ -136,7 +137,8 @@ export default async (): Promise<Resources> => ({
|
||||
StringEditorPopup: EditBoxPopup,
|
||||
BooleanTruePresenter,
|
||||
EnumEditor,
|
||||
FilterTypePopup
|
||||
FilterTypePopup,
|
||||
ValueSelector
|
||||
},
|
||||
popup: {
|
||||
PositionElementAlignment
|
||||
|
@ -110,6 +110,11 @@ export class ListSelectionProvider implements SelectionFocusProvider {
|
||||
update (docs: Doc[]): void {
|
||||
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._current?.focus === undefined) {
|
||||
this.delegate(0, undefined, 'vertical')
|
||||
|
@ -215,6 +215,11 @@ export interface ActionCategory extends Doc, UXObject {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ActionGroup = 'create' | 'edit' | 'associate' | 'copy' | 'tools' | 'other'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -224,6 +229,9 @@ export interface Action<T extends Doc = Doc, P = Record<string, any>> extends Do
|
||||
// Action implementation parameters
|
||||
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.
|
||||
// single - one object is required
|
||||
// any - one or multiple objects are required
|
||||
@ -275,8 +283,9 @@ export interface ViewContext {
|
||||
mode: ViewContextType | ViewContextType[]
|
||||
// Active application
|
||||
application?: Ref<Doc>
|
||||
|
||||
// Optional groupping
|
||||
group?: string
|
||||
group?: ActionGroup
|
||||
}
|
||||
|
||||
/**
|
||||
@ -435,7 +444,8 @@ const view = plugin(viewId, {
|
||||
ObjectPresenter: '' as AnyComponent,
|
||||
EditDoc: '' as AnyComponent,
|
||||
SpacePresenter: '' as AnyComponent,
|
||||
BooleanTruePresenter: '' as AnyComponent
|
||||
BooleanTruePresenter: '' as AnyComponent,
|
||||
ValueSelector: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
CustomizeView: '' as IntlString,
|
||||
@ -499,11 +509,33 @@ const view = plugin(viewId, {
|
||||
value?: string
|
||||
values?: string
|
||||
props?: Record<string, any>
|
||||
// Will copy values from selection document to props
|
||||
fillProps?: Record<string, string>
|
||||
}>,
|
||||
ShowEditor: '' as ViewAction<{
|
||||
element?: PopupPosAlignment | Resource<(e?: Event) => PopupAlignment | undefined>
|
||||
attribute: string
|
||||
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