Fix Actions (#2132)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-06-24 19:36:08 +07:00 committed by GitHub
parent 82c8d70d37
commit d935e9b963
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1219 additions and 356 deletions

View File

@ -2,15 +2,23 @@
## 0.6.29 (upcoming)
Platform:
- Object selector actions
Tracker:
- 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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,14 +44,13 @@
},
(result) => {
objects = result
dispatch('content', objects)
},
{
...options
}
)
$: dispatch('content', objects)
function getStateObjects (
objects: Item[],
state: TypeState,

View File

@ -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}
<span class="label">
<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>
{#if multiSelect}
<div class="check pointer-events-none">
<CheckBox checked={selectedElements.has(obj._id)} primary />
</div>
{/if}
</button>

View File

@ -26,6 +26,15 @@
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 {
max-height: calc(100vh - 2rem);
@ -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;

View File

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

View File

@ -38,6 +38,7 @@
"DaysAfter": "{days, plural, =1 {через день} other {через # дней}}",
"NoActionsDefined": "Нет доступных действий",
"Incoming": "Входящие",
"HoursLabel": "Часы"
"HoursLabel": "Часы",
"Back": "Назад"
}
}

View File

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

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

View File

@ -30,5 +30,6 @@
zIndex={(i + 1) * 500}
top={$modal.length - 1 === i}
close={popup.close}
overlay={popup.options.overlay}
/>
{/each}

View File

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

View File

@ -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}
<ListView bind:this={list} count={filteredObjects.length} bind:selection>
<svelte:fragment slot="item" let:item={itemId}>
{@const item = filteredObjects[itemId]}
<button
class="menu-item"
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={'small'} />
<Icon icon={IconCheck} {size} />
{/if}
</div>
{/if}
<div class="icon"><Icon icon={item.icon} size={'small'} /></div>
<span class="label">
{#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>
</button>
{/each}
</svelte:fragment>
</ListView>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +41,9 @@
}
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]
for (const docValue of vv) {
if ('_id' in docValue && parentIssue !== undefined && parentIssue?._id !== docValue.attachedTo) {
let rank: string | null = null
if (parentIssue) {
@ -54,19 +56,19 @@
rank = calcRank(lastAttachedIssue, undefined)
}
await client.update(value, {
await client.update(docValue, {
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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
]

View File

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

View File

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

View File

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

View File

@ -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,7 +195,35 @@
/>
</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>
{#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>
@ -249,6 +281,7 @@
</ListView>
</div>
</div>
{/if}
</div>
<style lang="scss">

View File

@ -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,14 +31,28 @@
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) => ({
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
})

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

View File

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

View File

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

View File

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