Keyboard popup (#1625)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2022-05-04 15:08:12 +07:00
committed by GitHub
parent 0cec2cb1d1
commit 8d7e3078e1
63 changed files with 1627 additions and 909 deletions
-3
View File
@@ -94,9 +94,6 @@ function getId (d: Doc): Ref<Doc> {
} else if (d._class === ('workbench:class:Application' as Ref<Doc>)) {
const cr = d as any
return ('workbench.app.' + (cr.label as string)) as Ref<Doc>
} else if (d._class === ('view:class:ActionTarget' as Ref<Doc>)) {
const cr = d as any
return ((cr.target as string) + '.' + (cr.action as string)) as Ref<Doc>
} else if (d._class === ('server-core:class:Trigger' as Ref<Doc>)) {
const cr = d as any
return cr.trigger as string as Ref<Doc>
+15 -12
View File
@@ -37,7 +37,7 @@ import chunter from '@anticrm/model-chunter'
import contact from '@anticrm/model-contact'
import core, { TAttachedDoc } from '@anticrm/model-core'
import { TSpaceWithStates } from '@anticrm/model-task'
import view from '@anticrm/model-view'
import view, { createAction } from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import notification from '@anticrm/notification'
import calendar from './plugin'
@@ -153,25 +153,28 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
view.class.Action,
view.class.ActionCategory,
core.space.Model,
{ label: calendar.string.Calendar, visible: true },
calendar.category.Calendar
)
createAction(
builder,
{
action: calendar.actionImpl.SaveEventReminder,
label: calendar.string.RemindMeAt,
icon: calendar.icon.Reminder,
action: calendar.actionImpl.SaveEventReminder,
singleInput: true
input: 'focus',
category: calendar.category.Calendar,
target: calendar.class.Event,
context: {
mode: 'context'
}
},
calendar.action.SaveEventReminder
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: calendar.class.Event,
action: calendar.action.SaveEventReminder,
context: {
mode: 'context'
}
})
builder.mixin(calendar.mixin.Reminder, core.class.Class, view.mixin.AttributePresenter, {
presenter: calendar.component.ReminderPresenter
})
+4 -1
View File
@@ -20,7 +20,7 @@ import { Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { mergeIds } from '@anticrm/platform'
import { AnyComponent } from '@anticrm/ui'
import { Action, ViewAction, ViewletDescriptor } from '@anticrm/view'
import { Action, ActionCategory, ViewAction, ViewletDescriptor } from '@anticrm/view'
export default mergeIds(calendarId, calendar, {
component: {
@@ -32,6 +32,9 @@ export default mergeIds(calendarId, calendar, {
action: {
SaveEventReminder: '' as Ref<Action>
},
category: {
Calendar: '' as Ref<ActionCategory>
},
actionImpl: {
SaveEventReminder: '' as ViewAction
},
+51 -58
View File
@@ -20,10 +20,10 @@ import type {
ChunterMessage,
ChunterSpace,
Comment,
DirectMessage,
Message,
SavedMessages,
ThreadMessage,
DirectMessage
ThreadMessage
} from '@anticrm/chunter'
import contact, { Employee } from '@anticrm/contact'
import type { Account, Class, Doc, Domain, Ref, Space, Timestamp } from '@anticrm/core'
@@ -43,11 +43,11 @@ import {
} from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import core, { TAttachedDoc, TSpace } from '@anticrm/model-core'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import chunter from './plugin'
import notification from '@anticrm/model-notification'
import preference, { TPreference } from '@anticrm/model-preference'
import view, { createAction } from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import chunter from './plugin'
export const DOMAIN_CHUNTER = 'chunter' as Domain
export const DOMAIN_COMMENT = 'comment' as Domain
@@ -204,87 +204,80 @@ export function createModel (builder: Builder): void {
})
builder.createDoc(
view.class.Action,
view.class.ActionCategory,
core.space.Model,
{ label: chunter.string.Chat, visible: true },
chunter.category.Chunter
)
createAction(
builder,
{
label: chunter.string.MarkUnread,
action: chunter.actionImpl.MarkUnread,
singleInput: true
label: chunter.string.MarkUnread,
input: 'focus',
category: chunter.category.Chunter,
target: chunter.class.Message,
context: {
mode: 'context'
}
},
chunter.action.MarkUnread
)
builder.createDoc(
view.class.Action,
core.space.Model,
createAction(
builder,
{
label: chunter.string.MarkUnread,
action: chunter.actionImpl.MarkCommentUnread,
singleInput: true
input: 'focus',
category: chunter.category.Chunter,
target: chunter.class.ThreadMessage,
context: {
mode: 'context'
}
},
chunter.action.MarkCommentUnread
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: chunter.class.Message,
action: chunter.action.MarkUnread,
context: {
mode: 'context'
}
})
builder.createDoc(
view.class.Action,
core.space.Model,
createAction(
builder,
{
action: chunter.actionImpl.ArchiveChannel,
label: chunter.string.ArchiveChannel,
icon: view.icon.Archive,
action: chunter.actionImpl.ArchiveChannel
input: 'focus',
category: chunter.category.Chunter,
target: chunter.class.Channel,
query: {
archived: false
},
context: {
mode: 'context'
}
},
chunter.action.ArchiveChannel
)
builder.createDoc(
view.class.Action,
core.space.Model,
createAction(
builder,
{
action: chunter.actionImpl.UnarchiveChannel,
label: chunter.string.UnarchiveChannel,
icon: view.icon.Archive,
action: chunter.actionImpl.UnarchiveChannel
input: 'focus',
category: chunter.category.Chunter,
target: chunter.class.Channel,
query: {
archived: true
},
context: {
mode: 'context'
}
},
chunter.action.UnarchiveChannel
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: chunter.class.Channel,
action: chunter.action.ArchiveChannel,
query: {
archived: false
},
context: {
mode: 'context'
}
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: chunter.class.Channel,
action: chunter.action.UnarchiveChannel,
query: {
archived: true
},
context: {
mode: 'context'
}
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: chunter.class.ThreadMessage,
action: chunter.action.MarkCommentUnread,
context: {
mode: 'context'
}
})
builder.createDoc(
workbench.class.Application,
core.space.Model,
+4 -1
View File
@@ -20,7 +20,7 @@ import type { Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { mergeIds } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import type { Action, ViewAction, ViewletDescriptor } from '@anticrm/view'
import type { Action, ActionCategory, ViewAction, ViewletDescriptor } from '@anticrm/view'
export default mergeIds(chunterId, chunter, {
component: {
@@ -43,6 +43,9 @@ export default mergeIds(chunterId, chunter, {
ArchiveChannel: '' as ViewAction,
UnarchiveChannel: '' as ViewAction
},
category: {
Chunter: '' as Ref<ActionCategory>
},
string: {
ApplicationLabelChunter: '' as IntlString,
LeftComment: '' as IntlString,
+6 -2
View File
@@ -31,7 +31,7 @@ import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
import presentation from '@anticrm/model-presentation'
import view, { actionTarget } from '@anticrm/model-view'
import view, { actionTemplates, createAction } from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import type { Asset, IntlString } from '@anticrm/platform'
import contact from './plugin'
@@ -290,7 +290,11 @@ export function createModel (builder: Builder): void {
contact.completion.OrganizationCategory
)
actionTarget(builder, view.action.Open, contact.class.Contact, { mode: ['browser', 'context'] })
createAction(builder, {
...actionTemplates.open,
target: contact.class.Contact,
context: { mode: ['browser', 'context'] }
})
}
export { contactOperation } from './migration'
+2 -1
View File
@@ -35,6 +35,7 @@
"@anticrm/inventory": "~0.6.0",
"@anticrm/inventory-resources": "~0.6.0",
"@anticrm/view": "~0.6.0",
"@anticrm/workbench": "~0.6.1"
"@anticrm/workbench": "~0.6.1",
"@anticrm/model-view": "~0.6.0"
}
}
+14 -14
View File
@@ -23,6 +23,7 @@ import workbench from '@anticrm/model-workbench'
import type {} from '@anticrm/view'
import view from '@anticrm/view'
import inventory from './plugin'
import { createAction } from '@anticrm/model-view'
export const DOMAIN_INVENTORY = 'inventory' as Domain
@Model(inventory.class.Category, core.class.AttachedDoc, DOMAIN_INVENTORY)
@@ -131,25 +132,24 @@ export function createModel (builder: Builder): void {
inventory.app.Inventory
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: inventory.string.Inventory, visible: true },
inventory.category.Inventory
)
createAction(builder, {
label: inventory.string.CreateSubcategory,
icon: inventory.icon.Categories,
action: inventory.actionImpl.CreateSubcategory,
input: 'focus',
category: inventory.category.Inventory,
target: inventory.class.Category,
action: inventory.action.CreateSubcategory,
context: {
mode: ['context', 'browser']
}
})
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: inventory.string.CreateSubcategory,
icon: inventory.icon.Categories,
action: inventory.actionImpl.CreateSubcategory,
singleInput: true
},
inventory.action.CreateSubcategory
)
}
export { default } from './plugin'
+4 -1
View File
@@ -19,7 +19,7 @@ import { inventoryId } from '@anticrm/inventory'
import inventory from '@anticrm/inventory-resources/src/plugin'
import { mergeIds } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import { Action, ViewAction } from '@anticrm/view'
import { Action, ActionCategory, ViewAction } from '@anticrm/view'
export default mergeIds(inventoryId, inventory, {
action: {
@@ -28,6 +28,9 @@ export default mergeIds(inventoryId, inventory, {
actionImpl: {
CreateSubcategory: '' as ViewAction
},
category: {
Inventory: '' as Ref<ActionCategory>
},
component: {
Categories: '' as AnyComponent,
Products: '' as AnyComponent,
+104 -88
View File
@@ -36,10 +36,12 @@ import contact, { TPerson } from '@anticrm/model-contact'
import core, { TSpace } from '@anticrm/model-core'
import presentation from '@anticrm/model-presentation'
import tags from '@anticrm/model-tags'
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
import view, { actionTarget, createAction } from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import task, { TSpaceWithStates, TTask, actionTemplates } from '@anticrm/model-task'
import view, { createAction, actionTemplates as viewTemplates } from '@anticrm/model-view'
import workbench, { Application, createNavigateAction } from '@anticrm/model-workbench'
import { IntlString } from '@anticrm/platform'
import { Applicant, Candidate, Candidates, Vacancy } from '@anticrm/recruit'
import { KeyBinding } from '@anticrm/view'
import recruit from './plugin'
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
import { TOpinion, TReview, TReviewCategory } from './review-model'
@@ -139,6 +141,13 @@ export function createModel (builder: Builder): void {
component: recruit.component.CreateCandidate
})
const vacanciesId = 'vacancies'
const candidatesId = 'candidates'
const skillsId = 'skills'
const applicantsId = 'applicants'
const archiveId = 'archive'
const assignedId = 'assigned'
builder.createDoc(
workbench.class.Application,
core.space.Model,
@@ -157,7 +166,7 @@ export function createModel (builder: Builder): void {
],
specials: [
{
id: 'vacancies',
id: vacanciesId,
component: recruit.component.Vacancies,
icon: recruit.icon.Vacancy,
label: recruit.string.Vacancies,
@@ -165,7 +174,7 @@ export function createModel (builder: Builder): void {
position: 'bottom'
},
{
id: 'applicants',
id: applicantsId,
component: recruit.component.ApplicationsView,
icon: recruit.icon.Application,
label: recruit.string.Applications,
@@ -173,7 +182,7 @@ export function createModel (builder: Builder): void {
position: 'bottom'
},
{
id: 'candidates',
id: candidatesId,
component: recruit.component.Candidates,
icon: contact.icon.Person,
label: recruit.string.Candidates,
@@ -181,7 +190,7 @@ export function createModel (builder: Builder): void {
position: 'bottom'
},
{
id: 'archive',
id: archiveId,
component: workbench.component.Archive,
icon: view.icon.Archive,
label: workbench.string.Archive,
@@ -190,7 +199,7 @@ export function createModel (builder: Builder): void {
spaceClass: recruit.class.Vacancy
},
{
id: 'skills',
id: skillsId,
component: recruit.component.SkillsView,
icon: tags.icon.Tags,
label: recruit.string.SkillsLabel,
@@ -198,7 +207,7 @@ export function createModel (builder: Builder): void {
position: 'bottom'
},
{
id: 'assigned',
id: assignedId,
label: task.string.Assigned,
icon: task.icon.Task,
component: task.component.AssignedTasks,
@@ -352,40 +361,46 @@ export function createModel (builder: Builder): void {
validator: recruit.validator.ApplicantValidator
})
createAction(
builder,
recruit.action.CreateApplication,
recruit.string.CreateAnApplication,
recruit.actionImpl.CreateApplication,
{
icon: recruit.icon.Create,
singleInput: true
}
)
actionTarget(builder, recruit.action.CreateApplication, contact.class.Person, { mode: ['context', 'browser'] })
createAction(
builder,
recruit.action.CreateCandidate,
recruit.string.CreateCandidate,
recruit.actionImpl.CreateCandidate,
{
icon: recruit.icon.Create,
keyBinding: ['c'],
singleInput: true
}
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: recruit.string.RecruitApplication, visible: true },
recruit.category.Recruit
)
actionTarget(builder, recruit.action.CreateCandidate, core.class.Doc, {
mode: ['workbench', 'browser'],
application: recruit.app.Recruit
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: recruit.component.CreateApplication,
_id: 'candidate',
element: 'top',
props: {
preserveCandidate: true
}
},
label: recruit.string.CreateAnApplication,
icon: recruit.icon.Create,
input: 'focus',
category: recruit.category.Recruit,
target: contact.class.Person,
context: { mode: ['context', 'browser'] }
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.mixin.Candidate,
action: task.action.CreateTask,
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: recruit.component.CreateCandidate,
element: 'top'
},
label: recruit.string.CreateCandidate,
icon: recruit.icon.Create,
keyBinding: ['c'],
input: 'none',
category: recruit.category.Recruit,
target: core.class.Doc,
context: {
mode: ['context', 'browser']
mode: ['workbench', 'browser'],
application: recruit.app.Recruit
}
})
@@ -411,55 +426,21 @@ export function createModel (builder: Builder): void {
recruit.completion.ApplicationCategory
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
createAction(builder, { ...actionTemplates.archiveSpace, target: recruit.class.Vacancy })
createAction(builder, { ...actionTemplates.unarchiveSpace, target: recruit.class.Vacancy })
createAction(builder, {
label: recruit.string.EditVacancy,
icon: recruit.icon.Vacancy,
action: view.actionImpl.ShowPanel,
actionProps: {
component: recruit.component.EditVacancy,
element: 'right'
},
input: 'focus',
category: recruit.category.Recruit,
keyBinding: ['e'],
target: recruit.class.Vacancy,
action: task.action.ArchiveSpace,
query: {
archived: false
},
context: {
mode: ['context', 'browser']
}
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.Vacancy,
action: task.action.UnarchiveSpace,
query: {
archived: true
},
context: {
mode: ['context', 'browser']
}
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.ReviewCategory,
action: task.action.UnarchiveSpace,
query: {
archived: true
},
context: {
mode: ['context', 'browser']
}
})
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: recruit.string.EditVacancy,
icon: recruit.icon.Vacancy,
action: recruit.actionImpl.EditVacancy,
singleInput: true
},
recruit.action.EditVacancy
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.Vacancy,
action: recruit.action.EditVacancy,
query: {},
context: {
mode: ['context', 'browser']
}
@@ -470,8 +451,43 @@ export function createModel (builder: Builder): void {
})
createReviewModel(builder)
actionTarget(builder, view.action.Open, recruit.class.Vacancy, { mode: ['browser', 'context'] })
actionTarget(builder, view.action.Open, recruit.class.Applicant, { mode: ['browser', 'context'] })
// createAction(builder, { ...viewTemplates.open, target: recruit.class.Vacancy, context: { mode: ['browser', 'context'] } })
createAction(builder, {
...viewTemplates.open,
target: recruit.class.Applicant,
context: { mode: ['browser', 'context'] }
})
function createGotoSpecialAction (builder: Builder, id: string, key: KeyBinding, label: IntlString): void {
createNavigateAction(builder, key, label, {
application: recruit.app.Recruit as Ref<Application>,
mode: 'special',
special: id
})
}
createGotoSpecialAction(builder, candidatesId, 'g->e', recruit.string.GotoCandidates)
createGotoSpecialAction(builder, vacanciesId, 'g->v', recruit.string.GotoVacancies)
createGotoSpecialAction(builder, skillsId, 'g->s', recruit.string.GotoSkills)
createGotoSpecialAction(builder, assignedId, 'g->h', recruit.string.GotoAssigned)
createGotoSpecialAction(builder, applicantsId, 'g->a', recruit.string.GotoApplicants)
createAction(builder, {
action: workbench.actionImpl.Navigate,
actionProps: {
mode: 'app',
application: recruit.app.Recruit as Ref<Application>,
special: candidatesId
},
label: recruit.string.GotoRecruitApplication,
icon: view.icon.ArrowRight,
input: 'none',
category: view.category.Navigation,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
}
})
}
export { recruitOperation } from './migration'
+11 -10
View File
@@ -21,23 +21,18 @@ import { recruitId } from '@anticrm/recruit'
import recruit from '@anticrm/recruit-resources/src/plugin'
import { KanbanTemplate } from '@anticrm/task'
import type { AnyComponent } from '@anticrm/ui'
import type { Action, ViewAction } from '@anticrm/view'
import type { Action, ActionCategory, ViewAction } from '@anticrm/view'
export default mergeIds(recruitId, recruit, {
action: {
CreateCandidate: '' as Ref<Action>,
CreateApplication: '' as Ref<Action>,
EditVacancy: '' as Ref<Action>,
CreateReview: '' as Ref<Action>,
CreateOpinion: '' as Ref<Action>
},
actionImpl: {
CreateCandidate: '' as ViewAction,
CreateApplication: '' as ViewAction,
EditVacancy: '' as ViewAction,
CreateReview: '' as ViewAction,
CreateOpinion: '' as ViewAction
},
category: {
Recruit: '' as Ref<ActionCategory>
},
string: {
ApplicationShort: '' as IntlString,
ApplicationsShort: '' as IntlString,
@@ -50,7 +45,13 @@ export default mergeIds(recruitId, recruit, {
Due: '' as IntlString,
Source: '' as IntlString,
ManageVacancyStatuses: '' as IntlString,
EditVacancy: '' as IntlString
EditVacancy: '' as IntlString,
GotoCandidates: '' as IntlString,
GotoVacancies: '' as IntlString,
GotoSkills: '' as IntlString,
GotoAssigned: '' as IntlString,
GotoApplicants: '' as IntlString,
GotoRecruitApplication: '' as IntlString
},
validator: {
ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>,
+27 -37
View File
@@ -3,8 +3,8 @@ import { Builder } from '@anticrm/model'
import calendar from '@anticrm/model-calendar'
import contact from '@anticrm/model-contact'
import core from '@anticrm/model-core'
import task from '@anticrm/model-task'
import view from '@anticrm/model-view'
import { actionTemplates } from '@anticrm/model-task'
import view, { createAction } from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import { Review } from '@anticrm/recruit'
import { BuildModelKey } from '@anticrm/view'
@@ -49,26 +49,22 @@ export function createReviewModel (builder: Builder): void {
createTableViewlet(builder)
builder.createDoc(
view.class.Action,
core.space.Model,
createAction(
builder,
{
label: recruit.string.CreateOpinion,
icon: recruit.icon.Create,
action: recruit.actionImpl.CreateOpinion,
singleInput: true
input: 'focus',
category: recruit.category.Recruit,
target: recruit.class.Review,
context: {
mode: ['context', 'browser']
}
},
recruit.action.CreateOpinion
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.Review,
action: recruit.action.CreateOpinion,
context: {
mode: ['context', 'browser']
}
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.ObjectEditor, {
editor: recruit.component.EditReview
})
@@ -89,35 +85,29 @@ export function createReviewModel (builder: Builder): void {
editor: recruit.component.EditReview
})
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: recruit.string.CreateReview,
icon: recruit.icon.Create,
action: recruit.actionImpl.CreateReview,
singleInput: true
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: recruit.component.CreateReview,
_id: 'candidate',
_space: 'space',
element: 'top',
props: {
preserveCandidate: true
}
},
recruit.action.CreateReview
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.Applicant,
action: recruit.action.CreateReview,
label: recruit.string.CreateReview,
icon: recruit.icon.Create,
input: 'focus',
category: recruit.category.Recruit,
target: recruit.mixin.Candidate,
context: {
mode: ['context', 'browser']
}
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.ReviewCategory,
action: task.action.ArchiveSpace,
query: {
archived: false
},
context: {
mode: ['context', 'browser']
}
})
createAction(builder, { ...actionTemplates.archiveSpace, target: recruit.class.ReviewCategory })
createAction(builder, { ...actionTemplates.unarchiveSpace, target: recruit.class.ReviewCategory })
const reviewOptions: FindOptions<Review> = {
lookup: {
+94 -119
View File
@@ -36,9 +36,9 @@ import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@anticrm/model-core'
import presentation from '@anticrm/model-presentation'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import view, { actionTemplates as viewTemplates, createAction, template } from '@anticrm/model-view'
import { IntlString } from '@anticrm/platform'
import { ViewAction } from '@anticrm/view'
import type {
DoneState,
DoneStateTemplate,
@@ -59,10 +59,9 @@ import type {
WonStateTemplate
} from '@anticrm/task'
import { AnyComponent } from '@anticrm/ui'
import type { ActionTarget } from '@anticrm/view'
import task from './plugin'
export { taskOperation, createKanbanTemplate, createSequence } from './migration'
export { createKanbanTemplate, createSequence, taskOperation } from './migration'
export { default } from './plugin'
export const DOMAIN_TASK = 'task' as Domain
@@ -240,6 +239,59 @@ export class TDocWithRank extends TDoc {
rank!: string
}
/**
* @public
*/
export const actionTemplates = template({
editStatus: {
label: task.string.EditStates,
icon: view.icon.Statuses,
action: task.actionImpl.EditStatuses,
input: 'focus',
category: task.category.Task
},
archiveSpace: {
label: task.string.Archive,
icon: view.icon.Archive,
action: view.actionImpl.UpdateDocument as ViewAction,
actionProps: {
key: 'archived',
value: true,
ask: true,
label: task.string.Archive,
message: task.string.ArchiveConfirm
},
input: 'focus',
category: task.category.Task,
query: {
archived: false
},
context: {
mode: ['context', 'browser']
}
},
unarchiveSpace: {
label: task.string.Unarchive,
icon: view.icon.Archive,
action: view.actionImpl.UpdateDocument as ViewAction,
actionProps: {
key: 'archived',
ask: true,
value: false,
label: task.string.Unarchive,
message: task.string.UnarchiveConfirm
},
input: 'focus',
category: task.category.Task,
query: {
archived: true
},
context: {
mode: ['context', 'browser']
}
}
})
export function createModel (builder: Builder): void {
builder.createModel(
TDocWithRank,
@@ -262,13 +314,6 @@ export function createModel (builder: Builder): void {
TIssue,
TTodoItem
)
builder.mixin(task.class.Project, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: task.class.Issue,
createItemDialog: task.component.CreateTask,
createItemLabel: task.string.TaskCreateLabel
}
})
builder.createDoc(
view.class.ViewletDescriptor,
@@ -281,36 +326,6 @@ export function createModel (builder: Builder): void {
task.viewlet.StatusTable
)
// builder.createDoc(
// workbench.class.Application,
// core.space.Model,
// {
// label: task.string.ApplicationLabelTask,
// icon: task.icon.Task,
// hidden: false,
// navigatorModel: {
// spaces: [
// {
// label: task.string.Projects,
// spaceClass: task.class.Project,
// addSpaceLabel: task.string.CreateProject,
// createComponent: task.component.CreateProject
// }
// ],
// specials: [
// {
// id: 'assigned',
// label: task.string.Assigned,
// icon: task.icon.Task,
// component: task.component.AssignedTasks,
// position: 'top'
// }
// ]
// }
// },
// task.app.Tasks
// )
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: task.class.Issue,
descriptor: task.viewlet.StatusTable,
@@ -385,62 +400,27 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
view.class.Action,
view.class.ActionCategory,
core.space.Model,
{
label: task.string.CreateTask,
icon: task.icon.Task,
action: task.actionImpl.CreateTask,
singleInput: true
},
task.action.CreateTask
{ label: task.string.Task, visible: true },
task.category.Task
)
builder.createDoc(
view.class.Action,
core.space.Model,
createAction(
builder,
{
label: task.string.EditStates,
icon: view.icon.Statuses,
action: task.actionImpl.EditStatuses,
singleInput: true
...actionTemplates.editStatus,
target: task.class.SpaceWithStates,
query: {
archived: false
},
context: {
mode: ['context', 'browser']
}
},
task.action.EditStatuses
)
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: task.string.Archive,
icon: view.icon.Archive,
action: task.actionImpl.ArchiveSpace
},
task.action.ArchiveSpace
)
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: task.string.Unarchive,
icon: view.icon.Archive,
action: task.actionImpl.UnarchiveSpace
},
task.action.UnarchiveSpace
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: task.class.SpaceWithStates,
action: task.action.EditStatuses,
query: {
archived: false
},
context: {
mode: ['context', 'browser']
}
})
builder.mixin(task.class.State, core.class.Class, view.mixin.AttributeEditor, {
editor: task.component.StateEditor
})
@@ -476,47 +456,42 @@ export function createModel (builder: Builder): void {
presenter: task.component.TodoItemPresenter
})
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: task.string.MarkAsDone,
icon: task.icon.TodoCheck,
action: task.actionImpl.TodoItemMarkDone
createAction(builder, {
label: task.string.MarkAsDone,
icon: task.icon.TodoCheck,
action: view.actionImpl.UpdateDocument,
actionProps: {
key: 'done',
value: true
},
task.action.TodoItemMarkDone
)
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: task.string.MarkAsUndone,
icon: task.icon.TodoUnCheck,
action: task.actionImpl.TodoItemMarkUnDone
},
task.action.TodoItemMarkUnDone
)
builder.createDoc<ActionTarget<TodoItem>>(view.class.ActionTarget, core.space.Model, {
target: task.class.TodoItem,
action: task.action.TodoItemMarkDone,
input: 'focus',
category: task.category.Task,
query: {
done: false
},
target: task.class.TodoItem,
context: {
mode: ['context', 'browser']
}
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: task.class.TodoItem,
action: task.action.TodoItemMarkUnDone,
createAction(builder, {
label: task.string.MarkAsUndone,
icon: task.icon.TodoUnCheck,
action: view.actionImpl.UpdateDocument,
actionProps: {
key: 'done',
value: false
},
input: 'focus',
category: task.category.Task,
query: {
done: true
},
context: {
mode: ['context', 'browser']
}
},
target: task.class.TodoItem
})
builder.createDoc(
@@ -530,9 +505,9 @@ export function createModel (builder: Builder): void {
task.completion.IssueCategory
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
createAction(builder, {
...viewTemplates.move,
target: task.class.Task,
action: view.action.Move,
context: {
mode: ['context', 'browser']
}
+4 -6
View File
@@ -21,29 +21,27 @@ import { mergeIds } from '@anticrm/platform'
import { KanbanTemplate, taskId } from '@anticrm/task'
import task from '@anticrm/task-resources/src/plugin'
import type { AnyComponent } from '@anticrm/ui'
import type { Action, ViewAction } from '@anticrm/view'
import type { Action, ActionCategory, ViewAction } from '@anticrm/view'
export default mergeIds(taskId, task, {
action: {
CreateTask: '' as Ref<Action>,
EditStatuses: '' as Ref<Action>,
TodoItemMarkDone: '' as Ref<Action>,
TodoItemMarkUnDone: '' as Ref<Action>,
ArchiveSpace: '' as Ref<Action>,
UnarchiveSpace: '' as Ref<Action>
},
actionImpl: {
CreateTask: '' as ViewAction,
EditStatuses: '' as ViewAction,
TodoItemMarkDone: '' as ViewAction,
TodoItemMarkUnDone: '' as ViewAction,
ArchiveSpace: '' as ViewAction,
UnarchiveSpace: '' as ViewAction
},
category: {
Task: '' as Ref<ActionCategory>
},
component: {
ProjectView: '' as AnyComponent,
CreateProject: '' as AnyComponent,
CreateTask: '' as AnyComponent,
EditIssue: '' as AnyComponent,
TaskPresenter: '' as AnyComponent,
KanbanCard: '' as AnyComponent,
+47 -8
View File
@@ -34,18 +34,20 @@ import {
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
import { createAction } from '@anticrm/model-view'
import workbench, { createNavigateAction } from '@anticrm/model-workbench'
import { Asset, IntlString } from '@anticrm/platform'
import view, { KeyBinding } from '@anticrm/view'
import {
Document,
Issue,
IssuePriority,
IssueStatus,
Project,
IssueStatusCategory,
Project,
ProjectStatus,
Team
} from '@anticrm/tracker'
import workbench from '@anticrm/model-workbench'
import { Asset, IntlString } from '@anticrm/platform'
import tracker from './plugin'
export { trackerOperation } from './migration'
@@ -292,6 +294,12 @@ export function createModel (builder: Builder): void {
tracker.issueStatusCategory.Canceled
)
const issuesId = 'issues'
const activeId = 'active'
const backlogId = 'backlog'
const boardId = 'board'
const projectsId = 'projects'
builder.createDoc(
workbench.class.Application,
core.space.Model,
@@ -332,31 +340,31 @@ export function createModel (builder: Builder): void {
icon: tracker.icon.Home,
specials: [
{
id: 'issues',
id: issuesId,
label: tracker.string.Issues,
icon: tracker.icon.Issues,
component: tracker.component.Issues
},
{
id: 'active',
id: activeId,
label: tracker.string.Active,
// icon: tracker.icon.TrackerApplication,
component: tracker.component.Active
},
{
id: 'backlog',
id: backlogId,
label: tracker.string.Backlog,
// icon: tracker.icon.TrackerApplication,
component: tracker.component.Backlog
},
{
id: 'board',
id: boardId,
label: tracker.string.Board,
// icon: tracker.icon.TrackerApplication,
component: tracker.component.Board
},
{
id: 'projects',
id: projectsId,
label: tracker.string.Projects,
icon: tracker.icon.Projects,
component: tracker.component.Projects
@@ -369,4 +377,35 @@ export function createModel (builder: Builder): void {
},
tracker.app.Tracker
)
function createGotoSpecialAction (builder: Builder, id: string, key: KeyBinding, label: IntlString): void {
createNavigateAction(builder, key, label, {
application: tracker.app.Tracker,
mode: 'space',
spaceSpecial: id,
spaceClass: tracker.class.Team
})
}
createGotoSpecialAction(builder, issuesId, 'g->e', tracker.string.GotoIssues)
createGotoSpecialAction(builder, activeId, 'g->a', tracker.string.GotoActive)
createGotoSpecialAction(builder, backlogId, 'g->b', tracker.string.GotoBacklog)
createGotoSpecialAction(builder, boardId, 'g->d', tracker.string.GotoBoard)
createGotoSpecialAction(builder, projectsId, 'g->p', tracker.string.GotoProjects)
createAction(builder, {
action: workbench.actionImpl.Navigate,
actionProps: {
mode: 'app',
application: tracker.app.Tracker
},
label: tracker.string.GotoTrackerApplication,
icon: view.icon.ArrowRight,
input: 'none',
category: view.category.Navigation,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
}
})
}
+7 -1
View File
@@ -24,7 +24,13 @@ import { Application } from '@anticrm/workbench'
export default mergeIds(trackerId, tracker, {
string: {
TrackerApplication: '' as IntlString,
Teams: '' as IntlString
Teams: '' as IntlString,
GotoIssues: '' as IntlString,
GotoActive: '' as IntlString,
GotoBacklog: '' as IntlString,
GotoBoard: '' as IntlString,
GotoProjects: '' as IntlString,
GotoTrackerApplication: '' as IntlString
},
team: {
DefaultTeam: '' as Ref<Team>
+220 -113
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import type { Class, Client, Doc, DocumentQuery, Ref, Space } from '@anticrm/core'
import type { Class, Client, Data, Doc, DocumentQuery, Ref, Space } from '@anticrm/core'
import { DOMAIN_MODEL } from '@anticrm/core'
import { Builder, Mixin, Model } from '@anticrm/model'
import core, { TClass, TDoc } from '@anticrm/model-core'
@@ -21,7 +21,7 @@ import type { Asset, IntlString, Resource, Status } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import type {
Action,
ActionTarget,
ActionCategory,
AttributeEditor,
AttributePresenter,
HTMLPresenter,
@@ -37,8 +37,8 @@ import type {
SpaceName,
TextPresenter,
ViewAction,
ViewActionInput,
ViewContext,
ViewContextType,
Viewlet,
ViewletDescriptor
} from '@anticrm/view'
@@ -47,52 +47,13 @@ import view from './plugin'
export { viewOperation } from './migration'
export { ViewAction }
export function createAction (
export function createAction<T extends Doc = Doc, P = Record<string, any>> (
builder: Builder,
id: Ref<Action>,
label: IntlString,
action: ViewAction,
config?: {
icon?: Asset
keyBinding?: KeyBinding[]
singleInput?: boolean
}
data: Data<Action<T, P>>,
id?: Ref<Action>
): void {
builder.createDoc(
view.class.Action,
core.space.Model,
{
label,
icon: config?.icon,
keyBinding: config?.keyBinding,
singleInput: config?.singleInput,
action
},
id
)
}
export function actionTarget (
builder: Builder,
action: Ref<Action>,
target: Ref<Class<Doc>>,
options: {
mode: ViewContextType | ViewContextType[]
application?: Ref<Doc>
group?: string
override?: ViewAction
}
): void {
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target,
action,
context: {
mode: options.mode,
application: options.application,
group: options.group
},
override: options.override
})
const { label, ...adata } = data as Data<Action>
builder.createDoc(view.class.Action, core.space.Model, { label, ...adata }, id)
}
export function classPresenter (
@@ -168,16 +129,27 @@ export class TViewlet extends TDoc implements Viewlet {
@Model(view.class.Action, core.class.Doc, DOMAIN_MODEL)
export class TAction extends TDoc implements Action {
label!: IntlString
icon?: Asset
icon!: Asset
action!: ViewAction
actionProps!: Record<string, any>
input!: ViewActionInput
target!: Ref<Class<Doc>>
query!: DocumentQuery<Doc>
inputProps!: Record<string, Ref<Class<Doc>>>
keyBinding!: KeyBinding[]
description!: IntlString
category!: Ref<ActionCategory>
context!: ViewContext
}
@Model(view.class.ActionTarget, core.class.Doc, DOMAIN_MODEL)
export class TActionTarget extends TDoc implements ActionTarget {
target!: Ref<Class<Doc>>
action!: Ref<Action>
query!: DocumentQuery<Doc>
context!: ViewContext
@Model(view.class.ActionCategory, core.class.Doc, DOMAIN_MODEL)
export class TActionCategory extends TDoc implements ActionCategory {
label!: IntlString
icon?: Asset
visible!: boolean
}
@Mixin(view.mixin.IgnoreActions, core.class.Class)
@@ -207,6 +179,33 @@ export class TLinkPresenter extends TDoc implements LinkPresenter {
component!: AnyComponent
}
export type ActionTemplate = Partial<Data<Action>>
/**
* @public
*/
export function template<N extends Record<string, ActionTemplate>> (t: N): N {
return t
}
export const actionTemplates = template({
move: {
label: view.string.Move,
action: view.actionImpl.Move,
icon: view.icon.Move,
input: 'focus',
category: view.category.General
},
open: {
action: view.actionImpl.Open,
label: view.string.Open,
icon: view.icon.Open,
keyBinding: ['Enter'],
input: 'focus',
category: view.category.General
}
})
export function createModel (builder: Builder): void {
builder.createModel(
TAttributeEditor,
@@ -215,7 +214,7 @@ export function createModel (builder: Builder): void {
TViewletDescriptor,
TViewlet,
TAction,
TActionTarget,
TActionCategory,
TObjectValidator,
TObjectFactory,
TObjectEditorHeader,
@@ -237,6 +236,37 @@ export function createModel (builder: Builder): void {
classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor)
classPresenter(builder, core.class.Space, view.component.ObjectPresenter)
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: view.string.General, visible: true },
view.category.General
)
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: view.string.General, visible: false },
view.category.GeneralNavigation
)
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: view.string.Editor, visible: false },
view.category.Editor
)
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: view.string.Navigation, visible: true },
view.category.Navigation
)
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: view.string.MarkdownFormatting, visible: false },
view.category.MarkdownFormatting
)
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
@@ -248,74 +278,154 @@ export function createModel (builder: Builder): void {
view.viewlet.Table
)
createAction(builder, view.action.Delete, view.string.Delete, view.actionImpl.Delete, {
icon: view.icon.Delete,
keyBinding: ['Meta + Backspace', 'Ctrl + Backspace']
})
actionTarget(builder, view.action.Delete, core.class.Doc, { mode: ['context', 'browser'], group: 'tools' })
createAction(builder, view.action.Move, view.string.Move, view.actionImpl.Move, {
icon: view.icon.Move,
singleInput: true
})
createAction(
builder,
{
action: view.actionImpl.Delete,
label: view.string.Delete,
icon: view.icon.Delete,
keyBinding: ['Meta + Backspace', 'Ctrl + Backspace'],
category: view.category.General,
input: 'any',
target: core.class.Doc,
context: { mode: ['context', 'browser'], group: 'tools' }
},
view.action.Delete
)
// Keyboard actions.
createAction(builder, view.action.MoveUp, view.string.MoveUp, view.actionImpl.MoveUp, {
keyBinding: ['ArrowUp', 'keyK']
})
actionTarget(builder, view.action.MoveUp, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.MoveDown, view.string.MoveDown, view.actionImpl.MoveDown, {
keyBinding: ['ArrowDown', 'keyJ']
})
actionTarget(builder, view.action.MoveDown, core.class.Doc, { mode: 'browser' })
createAction(
builder,
{
action: view.actionImpl.MoveUp,
label: view.string.MoveUp,
keyBinding: ['ArrowUp', 'keyK'],
category: view.category.GeneralNavigation,
input: 'none',
target: core.class.Doc,
context: { mode: 'browser' }
},
view.action.MoveUp
)
createAction(builder, view.action.MoveLeft, view.string.MoveLeft, view.actionImpl.MoveLeft, {
keyBinding: ['ArrowLeft']
})
actionTarget(builder, view.action.MoveLeft, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.MoveRight, view.string.MoveRight, view.actionImpl.MoveRight, {
keyBinding: ['ArrowRight']
})
actionTarget(builder, view.action.MoveRight, core.class.Doc, { mode: 'browser' })
createAction(
builder,
{
action: view.actionImpl.MoveDown,
label: view.string.MoveDown,
keyBinding: ['ArrowDown', 'keyJ'],
category: view.category.GeneralNavigation,
input: 'none',
target: core.class.Doc,
context: { mode: 'browser' }
},
view.action.MoveDown
)
createAction(
builder,
{
action: view.actionImpl.MoveLeft,
label: view.string.MoveLeft,
keyBinding: ['ArrowLeft'],
category: view.category.GeneralNavigation,
input: 'none',
target: core.class.Doc,
context: { mode: 'browser' }
},
view.action.MoveLeft
)
createAction(
builder,
{
action: view.actionImpl.MoveRight,
label: view.string.MoveRight,
keyBinding: ['ArrowRight'],
category: view.category.GeneralNavigation,
input: 'none',
target: core.class.Doc,
context: { mode: 'browser' }
},
view.action.MoveRight
)
builder.mixin(core.class.Space, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.SpacePresenter
})
// Selection stuff
createAction(builder, view.action.SelectItem, view.string.SelectItem, view.actionImpl.SelectItem, {
keyBinding: ['keyX']
})
actionTarget(builder, view.action.SelectItem, core.class.Doc, { mode: 'browser' })
createAction(
builder,
{
label: view.string.SelectItem,
action: view.actionImpl.SelectItem,
keyBinding: ['keyX'],
category: view.category.General,
input: 'focus',
target: core.class.Doc,
context: { mode: 'browser' }
},
view.action.SelectItem
)
createAction(builder, view.action.SelectItemAll, view.string.SelectItemAll, view.actionImpl.SelectItemAll, {
keyBinding: ['meta + keyA', 'ctrl + keyA']
})
actionTarget(builder, view.action.SelectItemAll, core.class.Doc, { mode: 'browser' })
createAction(
builder,
{
label: view.string.SelectItemAll,
action: view.actionImpl.SelectItemAll,
keyBinding: ['meta + keyA', 'ctrl + keyA'],
category: view.category.General,
input: 'none',
target: core.class.Doc,
context: { mode: 'browser' }
},
view.action.SelectItemAll
)
createAction(builder, view.action.SelectItemNone, view.string.SelectItemNone, view.actionImpl.SelectItemNone, {
keyBinding: ['escape']
})
actionTarget(builder, view.action.SelectItemNone, core.class.Doc, { mode: 'browser' })
createAction(
builder,
{
action: view.actionImpl.SelectItemNone,
label: view.string.SelectItemNone,
keyBinding: ['escape'],
category: view.category.General,
input: 'selection',
target: core.class.Doc,
context: { mode: 'browser' }
},
view.action.SelectItemNone
)
createAction(builder, view.action.ShowActions, view.string.ShowActions, view.actionImpl.ShowActions, {
keyBinding: ['meta + keyK', 'ctrl + keyK']
})
actionTarget(builder, view.action.ShowActions, core.class.Doc, {
mode: ['workbench', 'browser', 'popup', 'panel', 'editor']
})
createAction(
builder,
{
action: view.actionImpl.ShowActions,
label: view.string.ShowActions,
keyBinding: ['meta + keyK', 'ctrl + keyK'],
category: view.category.GeneralNavigation,
input: 'none',
target: core.class.Doc,
context: {
mode: ['workbench', 'browser', 'popup', 'panel', 'editor', 'input']
}
},
view.action.ShowActions
)
createAction(builder, view.action.ShowPreview, view.string.ShowPreview, view.actionImpl.ShowPreview, {
keyBinding: ['Space'],
singleInput: true
})
actionTarget(builder, view.action.ShowPreview, core.class.Doc, { mode: 'browser' })
createAction(builder, view.action.Open, view.string.Open, view.actionImpl.Open, {
icon: view.icon.Open,
keyBinding: ['Enter'],
singleInput: true
})
createAction(
builder,
{
action: view.actionImpl.ShowPreview,
label: view.string.ShowPreview,
keyBinding: ['Space'],
input: 'focus',
category: view.category.General,
target: core.class.Doc,
context: { mode: 'browser' }
},
view.action.ShowPreview
)
builder.createDoc(view.class.LinkPresenter, core.space.Model, {
pattern: '(www.)?youtube.(com|ru)',
@@ -326,9 +436,6 @@ export function createModel (builder: Builder): void {
pattern: '(www.)?github.com/',
component: view.component.GithubPresenter
})
// Should be contributed via individual plugins.
// actionTarget(builder, view.action.Open, core.class.Doc, { mode: ['browser', 'context'] })
}
export default view
+6 -1
View File
@@ -92,6 +92,11 @@ export default mergeIds(viewId, view, {
SelectDown: '' as IntlString,
ShowPreview: '' as IntlString,
ShowActions: '' as IntlString,
Open: '' as IntlString
Open: '' as IntlString,
// Action categories
General: '' as IntlString,
Navigation: '' as IntlString,
Editor: '' as IntlString,
MarkdownFormatting: '' as IntlString
}
})
+3 -1
View File
@@ -31,6 +31,8 @@
"@anticrm/model-core": "~0.6.0",
"@anticrm/workbench": "~0.6.1",
"@anticrm/ui": "~0.6.0",
"@anticrm/view": "~0.6.0"
"@anticrm/view": "~0.6.0",
"@anticrm/model-view": "~0.6.0",
"@anticrm/workbench-resources": "~0.6.1"
}
}
+34 -2
View File
@@ -14,14 +14,17 @@
//
import type { IntlString, Asset } from '@anticrm/platform'
import { DOMAIN_MODEL } from '@anticrm/core'
import { Class, DOMAIN_MODEL, Ref, Space } from '@anticrm/core'
import { Model, Mixin, Builder, UX } from '@anticrm/model'
import type { Application, SpaceView, ViewConfiguration } from '@anticrm/workbench'
import view from '@anticrm/view'
import view, { KeyBinding } from '@anticrm/view'
import { createAction } from '@anticrm/model-view'
import core, { TDoc, TClass } from '@anticrm/model-core'
import workbench from './plugin'
export { Application }
@Model(workbench.class.Application, core.class.Doc, DOMAIN_MODEL)
@UX(workbench.string.Application)
export class TApplication extends TDoc implements Application {
@@ -43,3 +46,32 @@ export function createModel (builder: Builder): void {
}
export default workbench
export function createNavigateAction (
builder: Builder,
key: KeyBinding,
label: IntlString,
config: {
mode: 'app' | 'special' | 'space'
application: Ref<Application>
special?: string
space?: Ref<Space>
spaceClass?: Ref<Class<Space>>
spaceSpecial?: string
}
): void {
createAction(builder, {
action: workbench.actionImpl.Navigate,
actionProps: config,
label,
icon: view.icon.ArrowRight,
keyBinding: [key],
input: 'none',
category: view.category.Navigation,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser', 'editor', 'panel', 'popup'],
application: config.application
}
})
}
+2 -2
View File
@@ -16,7 +16,8 @@
import { Space } from '@anticrm/core'
import { IntlString, mergeIds, Resource } from '@anticrm/platform'
import { AnyComponent } from '@anticrm/ui'
import workbench, { workbenchId } from '@anticrm/workbench'
import { workbenchId } from '@anticrm/workbench'
import workbench from '@anticrm/workbench-resources/src/plugin'
export default mergeIds(workbenchId, workbench, {
component: {
@@ -25,7 +26,6 @@ export default mergeIds(workbenchId, workbench, {
SpaceBrowser: '' as AnyComponent
},
string: {
Archive: '' as IntlString,
Application: '' as IntlString,
SpaceBrowser: '' as IntlString
},
+1 -1
View File
@@ -75,7 +75,7 @@ function identify (result: Record<string, any>, prefix: string, namespace: Recor
for (const key in namespace) {
const value = namespace[key]
if (typeof result[key] === 'string') {
throw new Error(`'identify' overwrites '${key}'.`)
throw new Error(`'identify' overwrites '${key}' for ${prefix}`)
}
const ident = prefix + _ID_SEPARATOR + key
result[key] = typeof value === 'string' ? ident : identify(result[key] ?? {}, ident, value)
+4
View File
@@ -523,6 +523,10 @@ a.no-line {
.text-xs { font-size: .625rem; }
.text-sm { font-size: .75rem; }
.text-md { font-size: .8125rem; }
.text-base {
font-size: 1rem; /* 16px */
line-height: 1.5rem; /* 24px */
}
.text-lg { font-size: 1.125rem; }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
+4
View File
@@ -32,6 +32,10 @@
max-height: calc(100vh - 2rem);
height: auto;
}
&.width-40 {
max-width: 40rem !important;
width: 40rem !important;
}
.header {
border-bottom: 1px solid var(--popup-divider);
+1
View File
@@ -29,6 +29,7 @@ export type {
AnySvelteComponentWithProps,
Location,
PopupAlignment,
PopupPositionElement,
ButtonKind,
ButtonSize
} from './types'
+3 -10
View File
@@ -72,16 +72,9 @@ export interface PopupPositionElement {
h: HorizontalAlignment
}
}
export type PopupAlignment =
| PopupPositionElement
| null
| 'right'
| 'top'
| 'float'
| 'account'
| 'full'
| 'content'
| 'middle'
export type PopupPosAlignment = 'right' | 'top' | 'float' | 'account' | 'full' | 'content' | 'middle'
export type PopupAlignment = PopupPosAlignment | PopupPositionElement | null
export type TooltipAlignment = 'top' | 'bottom' | 'left' | 'right'
export type VerticalAlignment = 'top' | 'bottom'
export type HorizontalAlignment = 'left' | 'right'
+12 -6
View File
@@ -12,14 +12,14 @@
"VacancyCreateLabel": "Vacancy",
"VacancyPlaceholder": "Software Engineer",
"MakePrivateDescription": "Only members can see it",
"CreateAnApplication": "Create an application",
"CreateAnApplication": "Create an Application",
"NoApplicationsForCandidate": "There are no applications for this candidate.",
"CreateApplication": "Create Application",
"CreateApplication": "Create an Application",
"ApplicationCreateLabel": "Application",
"SelectVacancy": "Select vacancy",
"Candidate": "Candidate",
"CandidateCreateLabel": "Candidate",
"CreateCandidate": "Create Candidate",
"CreateCandidate": "Create an Candidate",
"AssignRecruiter": "Assign recruiter",
"UnAssignRecruiter": "Unassign recruiter",
"Recruiters": "Recruiters",
@@ -56,13 +56,13 @@
"ManageVacancyStatuses": "Manage vacancy statuses",
"EditVacancy": "Edit",
"FullDescription": "Full description",
"CreateReviewCategory": "Create review category",
"CreateReviewCategory": "Create Review category",
"ReviewCategoryName": "Review category title*",
"ReviewCategoryPlaceholder": "Interview",
"ReviewCategory": "Reviews",
"ReviewCategoryDescription": "Description",
"ThisReviewCategoryIsPrivate": "This category is private",
"CreateReview": "Schedule",
"CreateReview": "Schedule an Review",
"CreateReviewParams": "Schedule {label}",
"SelectReviewCategory": "select category",
"Reviews": "Reviews",
@@ -87,7 +87,13 @@
"NoParticipants": "No participants added",
"PersonsLabel": "{name}",
"AddDescription": "Add description",
"NumberSkills": "{count, plural, =0 {no skills} =1 {1 skill} other {# skills}}"
"NumberSkills": "{count, plural, =0 {no skills} =1 {1 skill} other {# skills}}",
"GotoCandidates": "Go to Candidates",
"GotoVacancies": "Go to Vacancies",
"GotoSkills": "Go to Skills",
"GotoAssigned": "Go to my Assigned",
"GotoApplicants": "Go to Applications",
"GotoRecruitApplication": "Switch to Recruit Application"
},
"status": {
"CandidateRequired": "Please select candidate",
+8 -1
View File
@@ -88,7 +88,14 @@
"NoParticipants": "Участники не добавлены",
"PersonsLabel": "{name}",
"AddDescription": "Add description",
"NumberSkills": "{count, plural, =0 {нет навыков} =1 {1 навык} =2 {2 навыка} other {# навыков}}"
"NumberSkills": "{count, plural, =0 {нет навыков} =1 {1 навык} =2 {2 навыка} other {# навыков}}",
"GotoCandidates": "Перейте к кандидатам",
"GotoVacancies": "Перейти к вакансиям",
"GotoSkills": "Перейти к навыкам",
"GotoAssigned": "Перейти к моим назначениям",
"GotoApplicants": "Перейти к претендентам",
"GotoRecruitApplication": "Перейти к Приложению Рекрутинг"
},
"status": {
"CandidateRequired": "Пожалуйста выберите кандидата",
@@ -61,7 +61,7 @@
const hierarchy = client.getHierarchy()
export function canClose (): boolean {
return candidate === undefined && assignee === undefined
return (preserveCandidate || candidate === undefined) && assignee === undefined
}
async function createApplication () {
@@ -179,23 +179,6 @@
}}
>
<StatusControl slot="error" {status} />
<!-- <div class="flex-between mt-2 mb-2">
<div class="card" class:empty={!selectedCandidate}>
{#if selectedCandidate}
<CandidateCard candidate={selectedCandidate} disabled />
{:else}
<Label label={recruit.status.CandidateRequired} />
{/if}
</div>
<div class="arrows"><ExpandRightDouble /></div>
<div class="card" class:empty={!selectedVacancy}>
{#if selectedVacancy}
<VacancyCard vacancy={selectedVacancy} disabled />
{:else}
<Label label={recruit.status.VacancyRequired} />
{/if}
</div>
</div> -->
<svelte:fragment slot="pool">
{#if !preserveCandidate}
<UserBox
@@ -18,8 +18,8 @@
import { createQuery } from '@anticrm/presentation'
import { Vacancy } from '@anticrm/recruit'
import { Button, getCurrentLocation, Icon, IconAdd, Label, navigate, Scroller, showPopup } from '@anticrm/ui'
import SearchEdit from '@anticrm/ui/src/components/SearchEdit.svelte'
import { Table } from '@anticrm/view-resources'
import { SearchEdit } from '@anticrm/ui'
import { TableBrowser } from '@anticrm/view-resources'
import recruit from '../plugin'
import CreateVacancy from './CreateVacancy.svelte'
@@ -90,7 +90,7 @@
<Button icon={IconAdd} label={recruit.string.VacancyCreateLabel} kind={'primary'} on:click={showCreateDialog} />
</div>
<Scroller tableFade>
<Table
<TableBrowser
_class={recruit.class.Vacancy}
config={[
{
@@ -128,6 +128,5 @@
archived: false
}}
showNotification
highlightRows
/>
</Scroller>
@@ -68,7 +68,7 @@
const hierarchy = client.getHierarchy()
export function canClose (): boolean {
return (!preserveCandidate ? candidate === undefined : true) && title.length === 0
return (preserveCandidate || candidate === undefined) && title.length === 0
}
let spaceLabel: string = ''
+8 -29
View File
@@ -18,11 +18,12 @@ import { IntlString, OK, Resources, Severity, Status, translate } from '@anticrm
import { ObjectSearchResult } from '@anticrm/presentation'
import { Applicant, Review } from '@anticrm/recruit'
import task from '@anticrm/task'
import { showPanel, showPopup } from '@anticrm/ui'
import { showPopup } from '@anticrm/ui'
import ApplicationItem from './components/ApplicationItem.svelte'
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import Applications from './components/Applications.svelte'
import ApplicationsPresenter from './components/ApplicationsPresenter.svelte'
import ApplicationsView from './components/ApplicationsView.svelte'
import Candidates from './components/Candidates.svelte'
import CreateApplication from './components/CreateApplication.svelte'
import CreateCandidate from './components/CreateCandidate.svelte'
@@ -30,48 +31,30 @@ import CreateVacancy from './components/CreateVacancy.svelte'
import EditApplication from './components/EditApplication.svelte'
import EditVacancy from './components/EditVacancy.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import CreateReview from './components/review/CreateReview.svelte'
import CreateOpinion from './components/review/CreateOpinion.svelte'
import CreateReview from './components/review/CreateReview.svelte'
import CreateReviewCategory from './components/review/CreateReviewCategory.svelte'
import EditReview from './components/review/EditReview.svelte'
import EditReviewCategory from './components/review/EditReviewCategory.svelte'
import OpinionPresenter from './components/review/OpinionPresenter.svelte'
import OpinionsPresenter from './components/review/OpinionsPresenter.svelte'
import Opinions from './components/review/Opinions.svelte'
import OpinionsPresenter from './components/review/OpinionsPresenter.svelte'
import ReviewCategoryPresenter from './components/review/ReviewCategoryPresenter.svelte'
import ReviewPresenter from './components/review/ReviewPresenter.svelte'
import Reviews from './components/review/Reviews.svelte'
import SkillsView from './components/SkillsView.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import Vacancies from './components/Vacancies.svelte'
import VacancyItemPresenter from './components/VacancyItemPresenter.svelte'
import VacancyPresenter from './components/VacancyPresenter.svelte'
import VacancyCountPresenter from './components/VacancyCountPresenter.svelte'
import VacancyItemPresenter from './components/VacancyItemPresenter.svelte'
import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svelte'
import ReviewCategoryPresenter from './components/review/ReviewCategoryPresenter.svelte'
import ApplicationsView from './components/ApplicationsView.svelte'
import VacancyPresenter from './components/VacancyPresenter.svelte'
import recruit from './plugin'
async function createApplication (object: Doc): Promise<void> {
showPopup(CreateApplication, { candidate: object._id, preserveCandidate: true })
}
async function editVacancy (object: Doc): Promise<void> {
showPanel(recruit.component.EditVacancy, object._id, object._class, 'right')
}
async function createOpinion (object: Doc): Promise<void> {
showPopup(CreateOpinion, { space: object.space, review: object._id })
}
async function createReview (object: Doc): Promise<void> {
showPopup(CreateReview, { application: object._id, preserveApplication: true })
}
async function createCandidate (object: Doc | undefined, evt: Event): Promise<void> {
evt.preventDefault()
showPopup(CreateCandidate, {})
}
export async function applicantValidator (applicant: Applicant, client: Client): Promise<Status> {
if (applicant.attachedTo === undefined) {
return new Status(Severity.INFO, recruit.status.CandidateRequired, {})
@@ -143,11 +126,7 @@ export async function queryApplication (client: Client, search: string): Promise
export default async (): Promise<Resources> => ({
actionImpl: {
CreateApplication: createApplication,
EditVacancy: editVacancy,
CreateReview: createReview,
CreateOpinion: createOpinion,
CreateCandidate: createCandidate
CreateOpinion: createOpinion
},
validator: {
ApplicantValidator: applicantValidator,
-1
View File
@@ -20,7 +20,6 @@
"StateTemplateColor": "Color",
"KanbanTemplateTitle": "Title",
"Rank": "Rank",
"CreateTask": "Create task",
"TaskCreateLabel": "Task",
"EditStates": "Edit states",
"Archive": "Archive",
-1
View File
@@ -20,7 +20,6 @@
"StateTemplateColor": "Цвет",
"KanbanTemplateTitle": "Заголовок",
"Rank": "Ранк",
"CreateTask": "Создать задачу",
"TaskCreateLabel": "Задача",
"EditStates": "Редактировать статусы",
"Archive": "Архивировать",
@@ -21,7 +21,7 @@
import tags, { selectedTagElements, TagCategory, TagElement } from '@anticrm/tags'
import { DoneState, Task } from '@anticrm/task'
import { Component, Icon, Label, Scroller, SearchEdit } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import { TableBrowser } from '@anticrm/view-resources'
import task from '../plugin'
export let _class: Ref<Class<Task>> = task.class.Task
@@ -100,7 +100,7 @@
/>
<Scroller tableFade>
<Table
<TableBrowser
{_class}
config={[
'',
@@ -125,6 +125,5 @@
options={taskOptions}
query={resultQuery}
showNotification
highlightRows
/>
</Scroller>
@@ -1,136 +0,0 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee } from '@anticrm/contact'
import { AttachedData, Data, Doc, Ref, SortingOrder, Space } from '@anticrm/core'
import { generateId } from '@anticrm/core'
import { OK, Status } from '@anticrm/platform'
import { Card, getClient, UserBox } from '@anticrm/presentation'
import task, { calcRank, Issue, State } from '@anticrm/task'
import { EditBox, Grid, Status as StatusControl } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
export let space: Ref<Space>
export let parent: Pick<Doc, '_id' | '_class'> | undefined
let _space = space
$: _space = space
const status: Status = OK
let assignee: Ref<Employee> | null = null
const object: Data<Issue> = {
name: '',
description: '',
assignee: null,
number: 0,
attachedTo: task.global.Task,
attachedToClass: task.class.Issue,
collection: 'tasks',
state: '' as Ref<State>,
doneState: null,
rank: ''
}
const dispatch = createEventDispatcher()
const client = getClient()
const taskId: Ref<Issue> = generateId()
export function canClose (): boolean {
return object.name !== ''
}
async function createTask () {
const state = await client.findOne(task.class.State, { space: _space })
if (state === undefined) {
throw new Error('create task: state not found')
}
const sequence = await client.findOne(task.class.Sequence, { attachedTo: task.class.Issue })
if (sequence === undefined) {
throw new Error('sequence object not found')
}
const lastOne = await client.findOne(
task.class.Task,
{ state: state._id },
{ sort: { rank: SortingOrder.Descending } }
)
const incResult = await client.updateDoc(
task.class.Sequence,
task.space.Sequence,
sequence._id,
{
$inc: { sequence: 1 }
},
true
)
const value: AttachedData<Issue> = {
name: object.name,
description: object.description,
assignee,
number: (incResult as any).object.sequence,
doneState: null,
state: state._id,
rank: calcRank(lastOne, undefined)
}
await client.addCollection(
task.class.Issue,
_space,
parent?._id ?? task.global.Task,
parent?._class ?? task.class.Issue,
'tasks',
value,
taskId
)
}
</script>
<Card
label={plugin.string.CreateTask}
okAction={createTask}
canSave={object.name.length > 0 && _space != null}
spaceClass={task.class.Project}
spaceLabel={plugin.string.ProjectName}
spacePlaceholder={plugin.string.SelectProject}
bind:space={_space}
on:close={() => {
dispatch('close')
}}
>
<StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.5}>
<EditBox
label={plugin.string.TaskName}
bind:value={object.name}
icon={task.icon.Task}
placeholder={plugin.string.TaskNamePlaceholder}
maxWidth={'16rem'}
focus
/>
<UserBox
_class={contact.class.Employee}
label={plugin.string.TaskAssignee}
placeholder={plugin.string.AssignThisTask}
bind:value={assignee}
allowDeselect
titleDeselect={plugin.string.TaskUnAssign}
/>
</Grid>
</Card>
@@ -72,7 +72,6 @@
/* eslint-disable no-undef */
let kanbanUI: KanbanUI
let objects: Doc[] = []
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
kanbanUI.select(offset, of, dir)
})
@@ -114,7 +113,6 @@
rankFieldName={'rank'}
on:content={(evt) => {
listProvider.update(evt.detail)
objects = evt.detail
}}
on:obj-focus={(evt) => {
listProvider.updateFocus(evt.detail)
+4 -57
View File
@@ -14,13 +14,12 @@
// limitations under the License.
//
import { Class, Client, Doc, Ref } from '@anticrm/core'
import { Class, Client, Ref } from '@anticrm/core'
import { IntlString, Resources, translate } from '@anticrm/platform'
import { getClient, MessageBox, ObjectSearchResult } from '@anticrm/presentation'
import { SpaceWithStates, Task, TodoItem } from '@anticrm/task'
import { ObjectSearchResult } from '@anticrm/presentation'
import { SpaceWithStates, Task } from '@anticrm/task'
import { showPopup } from '@anticrm/ui'
import CreateProject from './components/CreateProject.svelte'
import CreateTask from './components/CreateTask.svelte'
import EditIssue from './components/EditIssue.svelte'
import KanbanTemplateEditor from './components/kanban/KanbanTemplateEditor.svelte'
import KanbanTemplateSelector from './components/kanban/KanbanTemplateSelector.svelte'
@@ -43,56 +42,10 @@ import TodoStatePresenter from './components/todos/TodoStatePresenter.svelte'
import AssignedTasks from './components/AssignedTasks.svelte'
import task from './plugin'
async function createTask (object: Doc): Promise<void> {
showPopup(CreateTask, { parent: object._id, space: object.space })
}
async function editStatuses (object: SpaceWithStates): Promise<void> {
showPopup(EditStatuses, { _id: object._id, spaceClass: object._class }, 'right')
}
async function toggleDone (value: boolean, object: TodoItem): Promise<void> {
await getClient().update(object, { done: value })
}
async function ArchiveSpace (object: SpaceWithStates): Promise<void> {
showPopup(
MessageBox,
{
label: task.string.Archive,
message: task.string.ArchiveConfirm
},
undefined,
(result: boolean) => {
if (result) {
const client = getClient()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
client.update(object, { archived: true })
}
}
)
}
async function UnarchiveSpace (object: SpaceWithStates): Promise<void> {
showPopup(
MessageBox,
{
label: task.string.Unarchive,
message: task.string.UnarchiveConfirm
},
undefined,
(result: boolean) => {
if (result) {
const client = getClient()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
client.update(object, { archived: false })
}
}
)
}
export async function queryTask<D extends Task> (
_class: Ref<Class<D>>,
client: Client,
@@ -136,7 +89,6 @@ export type StatesBarPosition = 'start' | 'middle' | 'end' | undefined
export default async (): Promise<Resources> => ({
component: {
CreateTask,
CreateProject,
TaskPresenter,
EditIssue,
@@ -158,12 +110,7 @@ export default async (): Promise<Resources> => ({
TodoItemsPopup
},
actionImpl: {
CreateTask: createTask,
EditStatuses: editStatuses,
TodoItemMarkDone: async (obj: TodoItem) => await toggleDone(true, obj),
TodoItemMarkUnDone: async (obj: TodoItem) => await toggleDone(false, obj),
ArchiveSpace,
UnarchiveSpace
EditStatuses: editStatuses
},
completion: {
IssueQuery: async (client: Client, query: string) => await queryTask(task.class.Issue, client, query)
-1
View File
@@ -23,7 +23,6 @@ export default mergeIds(taskId, task, {
CreateProject: '' as IntlString,
ProjectName: '' as IntlString,
ProjectNamePlaceholder: '' as IntlString,
CreateTask: '' as IntlString,
TaskCreateLabel: '' as IntlString,
TaskAssignee: '' as IntlString,
TaskNamePlaceholder: '' as IntlString,
+8 -1
View File
@@ -94,7 +94,14 @@
"PastWeek": "Past week",
"PastMonth": "Past month",
"CopyIssueUrl": "Copy Issue URL to clipboard",
"CopyIssueId": "Copy Issue ID to clipboard"
"CopyIssueId": "Copy Issue ID to clipboard",
"GotoIssues": "Go to issues",
"GotoActive": "Go to active issues",
"GotoBacklog": "Go to backlog",
"GotoBoard": "Go to issue board",
"GotoProjects": "Go to projects",
"GotoTrackerApplication": "Switch to Tracker Application"
},
"status": {}
}
+21 -14
View File
@@ -1,18 +1,18 @@
{
"string": {
"TrackerApplication": "Tracker",
"Teams": "Your teams",
"More": "More",
"Delete": "Delete",
"Open": "Open",
"Members": "Members",
"Inbox": "Inbox",
"MyIssues": "My issues",
"Issues": "Issues",
"Views": "Views",
"Active": "Active",
"Backlog": "Backlog",
"Board": "Board",
"TrackerApplication": "Трекер",
"Teams": "Команды",
"More": "Больше",
"Delete": "Удалить",
"Open": "Открыть",
"Members": "Участиники",
"Inbox": "Входящие",
"MyIssues": "Мои задачи",
"Issues": "Задачи",
"Views": "Отображения",
"Active": "Активные",
"Backlog": "Пул задач",
"Board": "Канбан",
"Projects": "Проекты",
"NewProject": "Новый проект",
"CreateProject": "Создать проект",
@@ -66,7 +66,14 @@
"DocumentColor": "",
"Rank": "",
"IssueTitlePlaceholder": "Имя задачи",
"IssueDescriptionPlaceholder": "Описание задачи"
"IssueDescriptionPlaceholder": "Описание задачи",
"GotoIssues": "Перейти к задачам",
"GotoActive": "Перейти к активным задачам",
"GotoBacklog": "Перейти к пулу задач",
"GotoBoard": "Перейти к канбану",
"GotoProjects": "Перейти к проекту",
"GotoTrackerApplication": "Go to Tracker Application"
},
"status": {}
}
+5
View File
@@ -46,4 +46,9 @@
<path d="M11.4,7.1C11.4,7.1,11.4,7.1,11.4,7.1c1.2-1.6,1.3-1.6,1.3-1.6C12.9,5,13,4.5,12.9,4c-0.1-0.5-0.4-0.9-0.8-1.2 c0,0-1.1-0.9-1.1-0.9c-0.8-0.7-2.1-0.6-2.8,0.3c0,0,0,0,0,0l-6.3,7.9c-0.3,0.4-0.4,0.9-0.3,1.4l0.5,2.3c0.1,0.2,0.3,0.4,0.5,0.4 c0,0,0,0,0,0l2.4,0c0.5,0,1-0.2,1.3-0.6C8.9,10.2,10.5,8.2,11.4,7.1C11.4,7.1,11.4,7.1,11.4,7.1z M8.9,2.8c0.3-0.4,1-0.5,1.4-0.1 c0,0,1.2,0.9,1.2,0.9c0.2,0.1,0.4,0.3,0.4,0.6c0.1,0.2,0,0.5-0.1,0.7c0,0-0.4,0.5-0.9,1.2L8.1,3.9L8.9,2.8z M5.5,12.9 C5.4,13,5.2,13.1,5,13.1l-2,0l-0.5-1.9c0-0.2,0-0.4,0.1-0.5l4.8-6l2.8,2.2C8.9,8.6,6.8,11.2,5.5,12.9z"/>
</symbol>
<symbol id='arrow-right' viewBox="0 0 16 16">
<path d="M13.334 8L13.6875 7.64645L14.0411 8L13.6875 8.35355L13.334 8ZM3.33398 8.5C3.05784 8.5 2.83398 8.27614 2.83398 8C2.83398 7.72386 3.05784 7.5 3.33398 7.5V8.5ZM9.68754 3.64645L13.6875 7.64645L12.9804 8.35355L8.98043 4.35355L9.68754 3.64645ZM13.6875 8.35355L9.68754 12.3536L8.98043 11.6464L12.9804 7.64645L13.6875 8.35355ZM13.334 8.5H3.33398V7.5H13.334V8.5Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

+18 -1
View File
@@ -15,6 +15,23 @@
"DeleteObjectConfirm": "Do you want to delete this {count, plural, =1 {object} other {# objects}}?",
"Open": "Open",
"Assignees": "Assignees",
"Labels": "Labels"
"Labels": "Labels",
"MoveLeft": "Move left",
"MoveRight": "Move right",
"MoveUp": "Move up",
"MoveDown": "Move down",
"SelectItem": "Select focused item",
"SelectItemAll": "Select all items",
"SelectItemNone": "Deselect all items",
"ShowPreview": "Show document preview",
"ShowActions": "Show actions popup",
"ActionPlaceholder": "type to filter...",
"General": "General",
"Navigation": "Navigation",
"Editor": "Editor",
"MarkdownFormatting": "Formatting"
}
}
+6 -1
View File
@@ -15,6 +15,11 @@
"DeleteObjectConfirm": "Вы действительно хотите удалить {count, plural, =1 {этот обьект} other {эти # обьекта}}?",
"Open": "Открыть",
"Assignees": "Исполнители",
"Labels": "Метки"
"Labels": "Метки",
"ActionPlaceholder": "введите для филтрации...",
"General": "Общее",
"Navigation": "Навигация",
"Editor": "Редактор",
"MarkdownFormatting": "Форматирование"
}
}
+2 -1
View File
@@ -25,7 +25,8 @@ loadMetadata(view.icon, {
MoreH: `${icons}#more-h`,
Archive: `${icons}#archive`,
Statuses: `${icons}#statuses`,
Open: `${icons}#open`
Open: `${icons}#open`,
ArrowRight: `${icons}#arrow-right`
})
addStringsLoader(viewId, async (lang: string) => await import(`../lang/${lang}.json`))
+121 -4
View File
@@ -1,9 +1,11 @@
import { Doc, Hierarchy } from '@anticrm/core'
import { getClient, MessageBox } from '@anticrm/presentation'
import { showPanel, showPopup } from '@anticrm/ui'
import { AnyComponent, closeTooltip, PopupPositionElement, showPanel, showPopup } from '@anticrm/ui'
import { ViewContext } from '@anticrm/view'
import MoveView from './components/Move.svelte'
import { contextStore } from './context'
import view from './plugin'
import { FocusSelection, focusStore, SelectDirection, selectionStore, previewDocument } from './selection'
import { FocusSelection, focusStore, previewDocument, SelectDirection, selectionStore } from './selection'
import { deleteObject } from './utils'
function Delete (object: Doc): void {
@@ -31,12 +33,17 @@ async function Move (object: Doc): Promise<void> {
}
let $focusStore: FocusSelection
focusStore.subscribe((it) => {
$focusStore = it
})
let $contextStore: ViewContext[]
contextStore.subscribe((it) => {
$contextStore = it
})
export function select (evt: Event | undefined, offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection): void {
closeTooltip()
if ($focusStore.provider?.select !== undefined) {
$focusStore.provider?.select(offset, of, direction)
evt?.preventDefault()
@@ -81,6 +88,8 @@ const MoveRight = (doc: Doc | undefined, evt: Event): void => select(evt, 1, doc
function ShowActions (doc: Doc | Doc[] | undefined, evt: Event): void {
evt.preventDefault()
showPopup(view.component.ActionsPopup, { viewContext: $contextStore[$contextStore.length - 1] }, 'top')
}
function ShowPreview (doc: Doc | undefined, evt: Event): void {
@@ -98,6 +107,111 @@ function Open (doc: Doc, evt: Event): void {
showPanel(view.component.EditDoc, doc._id, Hierarchy.mixinOrClass(doc), 'content')
}
/**
* Quick action for show panel
* Require props:
* - component - view.component.EditDoc or another component
* - element - position
* - right - some right component
*/
function ShowPanel (
doc: Doc | Doc[],
evt: Event,
props: {
component?: AnyComponent
element: PopupPositionElement
rightSection?: AnyComponent
}
): void {
if (Array.isArray(doc)) {
console.error('Wrong show Panel parameters')
return
}
evt.preventDefault()
showPanel(
props.component ?? view.component.EditDoc,
doc._id,
Hierarchy.mixinOrClass(doc),
props.element ?? 'content',
props.rightSection
)
}
/**
* Quick action for show popup
* Props:
* - _id - object id will be placed into
* - _class - object _class will be placed into
* - value - object itself will be placed into
* - values - all docs will be placed into
* - props - some basic props, will be merged with key, _class, value, values
*/
function ShowPopup (
doc: Doc | Doc[],
evt: Event,
props: {
component: AnyComponent
element: PopupPositionElement
_id?: string
_class?: string
_space?: string
value?: string
values?: string
props?: Record<string, any>
}
): void {
const docs = Array.isArray(doc) ? doc : [doc]
evt.preventDefault()
let cprops = {
...(props?.props ?? {})
}
if (docs.length > 0) {
cprops = {
...cprops,
...{
[props._id ?? '_id']: docs[0]._id,
[props._class ?? '_class']: docs[0]._class,
[props._space ?? 'space']: docs[0].space,
[props.value ?? 'value']: docs[0],
[props.values ?? 'values']: docs
}
}
}
showPopup(props.component, cprops, props.element)
}
function UpdateDocument (doc: Doc | Doc[], evt: Event, props: Record<string, any>): void {
async function update (): Promise<void> {
if (props?.key !== undefined && props?.value !== undefined) {
if (Array.isArray(doc)) {
for (const d of doc) {
await getClient().update(d, { [props.key]: props.value })
}
} else {
await getClient().update(doc, { [props.key]: props.value })
}
}
}
if (props?.ask === true) {
showPopup(
MessageBox,
{
label: props.label ?? view.string.LabelYes,
message: props.message ?? view.string.LabelYes
},
undefined,
(result: boolean) => {
if (result) {
void update()
}
}
)
} else {
void update()
}
}
/**
* @public
*/
@@ -113,5 +227,8 @@ export const actionImpl = {
SelectItemAll,
ShowActions,
ShowPreview,
Open
Open,
UpdateDocument,
ShowPanel,
ShowPopup
}
+67 -44
View File
@@ -14,10 +14,24 @@
// limitations under the License.
//
import type { Doc } from '@anticrm/core'
import core, { Class, Client, FindResult, matchQuery, Ref } from '@anticrm/core'
import type { Action, ActionTarget, ViewContext } from '@anticrm/view'
import type { Doc, WithLookup } from '@anticrm/core'
import core, { Class, Client, matchQuery, Ref } from '@anticrm/core'
import type { Action, ViewActionInput, ViewContextType } from '@anticrm/view'
import view from './plugin'
import { FocusSelection } from './selection'
/**
* @public
*/
export function getSelection (focusStore: FocusSelection, selectionStore: Doc[]): Doc[] {
let docs: Doc[] = []
if (selectionStore.find((it) => it._id === focusStore.focus?._id) === undefined && focusStore.focus !== undefined) {
docs = [focusStore.focus]
} else {
docs = selectionStore
}
return docs
}
/**
* @public
@@ -31,77 +45,86 @@ export async function getActions (
client: Client,
doc: Doc | Doc[],
derived: Ref<Class<Doc>> = core.class.Doc,
singleInput = false
): Promise<FindResult<Action>> {
const targets = await client.findAll(view.class.ActionTarget, {
'context.mode': 'context'
mode: ViewContextType = 'context'
): Promise<Action[]> {
const actions: Action[] = await client.findAll(view.class.Action, {
'context.mode': mode
})
const tmap = new Map(targets.map((it) => [it.action, it.context?.group]))
const categories: Record<string, number> = { top: 1, filter: 50, tools: 100 }
let filter = targets.map((it) => it.action)
let filteredActions = actions
if (Array.isArray(doc)) {
for (const d of doc) {
const b = filterActions(client, d, targets, derived)
filter = filter.filter((it) => b.includes(it))
filteredActions = filterActions(client, d, filteredActions, derived)
}
} else {
filter = filterActions(client, doc, targets, derived)
filteredActions = filterActions(client, doc, filteredActions, derived)
}
const result = await client.findAll(view.class.Action, {
_id: { $in: filter },
singleInput: { $in: [undefined, singleInput] }
})
result.sort((a, b) => {
const aTarget = categories[tmap.get(a._id) ?? 'top'] ?? 0
const bTarget = categories[tmap.get(b._id) ?? 'top'] ?? 0
const inputVal: ViewActionInput[] = ['none']
if (!Array.isArray(doc) || doc.length === 1) {
inputVal.push('focus')
inputVal.push('any')
}
if (Array.isArray(doc) && doc.length > 0) {
inputVal.push('selection')
inputVal.push('any')
}
filteredActions = filteredActions.filter((it) => inputVal.includes(it.input))
filteredActions.sort((a, b) => {
const aTarget = categories[a.context.group ?? 'top'] ?? 0
const bTarget = categories[b.context.group ?? 'top'] ?? 0
return aTarget - bTarget
})
return result
return filteredActions
}
export async function getContextActions (
client: Client,
target: Ref<Class<Doc>>,
context: ViewContext,
multiple: boolean
): Promise<FindResult<Action>> {
const desc = client.getHierarchy().getAncestors(target)
const targets = await client.findAll(view.class.ActionTarget, {
target: { $in: desc },
'context.mode': context.mode,
'context.application': { $in: [context.application, undefined] }
})
return await client.findAll(view.class.Action, {
_id: { $in: targets.map((it) => it.action) },
singleInput: { $in: [undefined, !multiple] }
})
doc: Doc | Doc[],
context: {
mode: ViewContextType
application?: Ref<Doc>
}
): Promise<Action[]> {
const result = await getActions(client, doc, undefined, context.mode)
if (context.application !== undefined) {
return result.filter((it) => it.context.application === context.application || it.context.application === undefined)
}
return result
}
function filterActions (
/**
* @public
*/
export function filterActions (
client: Client,
doc: Doc,
targets: ActionTarget[],
actions: Array<WithLookup<Action>>,
derived: Ref<Class<Doc>> = core.class.Doc
): Array<Ref<Action>> {
const result: Array<Ref<Action>> = []
): Array<WithLookup<Action>> {
const result: Array<WithLookup<Action>> = []
const hierarchy = client.getHierarchy()
const clazz = hierarchy.getClass(doc._class)
const ignoreActions = hierarchy.as(clazz, view.mixin.IgnoreActions)
const ignore = ignoreActions?.actions ?? []
for (const target of targets) {
if (ignore.includes(target.action)) {
for (const action of actions) {
if (ignore.includes(action._id)) {
continue
}
if (target.query !== undefined) {
const r = matchQuery([doc], target.query, doc._class, hierarchy)
if (action.query !== undefined) {
const r = matchQuery([doc], action.query, doc._class, hierarchy)
if (r.length === 0) {
continue
}
}
if (hierarchy.isDerived(doc._class, target.target) && client.getHierarchy().isDerived(target.target, derived)) {
result.push(target.action)
if (
(hierarchy.isDerived(doc._class, action.target) && client.getHierarchy().isDerived(action.target, derived)) ||
(hierarchy.isMixin(action.target) && hierarchy.hasMixin(doc, action.target))
) {
result.push(action)
}
}
return result
@@ -1,3 +1,17 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { generateId } from '@anticrm/core'
import { ViewContext } from '@anticrm/view'
@@ -1,11 +1,25 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import core, { Class, Doc, Ref, TxRemoveDoc } from '@anticrm/core'
import core, { Doc, Ref, TxRemoveDoc } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { addTxListener, getClient } from '@anticrm/presentation'
import { AnyComponent, Component } from '@anticrm/ui'
import { Action, ViewContext } from '@anticrm/view'
import { Action, ViewContextType } from '@anticrm/view'
import { fly } from 'svelte/transition'
import { getContextActions } from '../actions'
import { getContextActions, getSelection } from '../actions'
import { contextStore } from '../context'
import { focusStore, previewDocument, selectionStore } from '../selection'
import { getObjectPreview } from '../utils'
@@ -15,8 +29,6 @@
let actions: Action[] = []
let q = 0
$: selectionClass = $focusStore.focus?._class
addTxListener((tx) => {
if (tx._class === core.class.TxRemoveDoc) {
const docId = (tx as TxRemoveDoc<Doc>).objectId
@@ -28,56 +40,98 @@
}
})
// let lastKey: KeyboardEvent | undefined
let lastKey: KeyboardEvent | undefined
async function updateActions (
context: ViewContext,
selectionClass: Ref<Class<Doc>> | undefined,
multiSelection: boolean
context: {
mode: ViewContextType
application?: Ref<Doc>
},
focus: Doc | undefined | null,
selection: Doc[]
): Promise<void> {
const t = ++q
const r = await getContextActions(client, selectionClass ?? core.class.Doc, context, multiSelection)
let docs: Doc | Doc[] = []
if (selection.find((it) => it._id === focus?._id) === undefined && focus != null) {
docs = focus
} else {
docs = selection
}
const r = await getContextActions(client, docs, context)
if (t === q) {
actions = r
}
}
$: ctx = $contextStore[$contextStore.length - 1]
$: if (ctx !== undefined) updateActions(ctx, selectionClass, $selectionStore.length > 1)
function matchKey (key: KeyboardEvent, pattern: string): boolean {
const fp = (
$: ctx = $contextStore[$contextStore.length - 1]
$: if (ctx !== undefined) {
updateActions(
{ mode: ctx.mode as ViewContextType, application: ctx.application },
$focusStore.focus,
$selectionStore
)
}
function keyPrefix (key: KeyboardEvent): string {
return (
(key.altKey ? 'Alt + ' : '') +
(key.shiftKey ? 'Shift + ' : '') +
(key.metaKey ? 'Meta + ' : '') +
(key.ctrlKey ? 'Ctrl + ' : '') +
key.key
).toLowerCase()
const fp2 = (
(key.altKey ? 'Alt + ' : '') +
(key.shiftKey ? 'Shift + ' : '') +
(key.metaKey ? 'Meta + ' : '') +
(key.ctrlKey ? 'Ctrl + ' : '') +
key.code
).toLowerCase()
return fp === pattern.toLowerCase() || fp2 === pattern.toLowerCase()
(key.ctrlKey ? 'Ctrl + ' : '')
)
}
function m (s1: string, s2: string): boolean {
return s1 === s2.toLowerCase()
}
function matchKey (key: KeyboardEvent, pattern: string, lastKey?: KeyboardEvent): boolean {
const fp = (keyPrefix(key) + key.key).toLowerCase()
const fp2 = (keyPrefix(key) + key.code).toLowerCase()
const lc = pattern.toLowerCase()
if (m(fp, lc) || m(fp2, lc)) {
return true
}
if (lastKey !== undefined) {
// Check with last key
const pfp = (keyPrefix(key) + lastKey.key).toLowerCase()
const pfp2 = (keyPrefix(key) + lastKey.code).toLowerCase()
return m(`${pfp}->${fp}`, lc) || m(`${pfp2}->${fp}`, lc) || m(`${pfp}->${fp2}`, lc) || m(`${pfp2}->${fp2}`, lc)
}
return false
}
async function handleKeys (evt: KeyboardEvent): Promise<void> {
const targetTagName = (evt.target as any)?.tagName?.toLowerCase()
if (targetTagName === 'input' || targetTagName === 'button' || targetTagName === 'textarea') {
console.log('no keyboard because', targetTagName, evt.target)
let currentActions = actions
// For none we ignore all actions.
if (ctx.mode === 'none') {
return
}
// lastKey = evt
for (const a of actions) {
const docs = getSelection($focusStore, $selectionStore)
if (targetTagName === 'input' || targetTagName === 'button' || targetTagName === 'textarea') {
// Retrieve actual list of actions for input context
currentActions = await getContextActions(client, docs, { ...ctx, mode: 'input' })
}
for (const a of currentActions) {
// TODO: Handle multiple keys here
if (a.keyBinding?.find((it) => matchKey(evt, it)) !== undefined) {
if (a.keyBinding?.find((it) => matchKey(evt, it, lastKey)) !== undefined) {
lastKey = undefined
const action = await getResource(a.action)
if (action !== undefined) {
action($focusStore.focus, evt)
await action($focusStore.focus, evt, a.actionProps)
}
}
}
lastKey = evt
setTimeout(() => {
lastKey = undefined
}, 500)
}
let presenter: AnyComponent | undefined
@@ -0,0 +1,274 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { 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 { Action, ViewContext } from '@anticrm/view'
import { onMount } from 'svelte'
import { filterActions, getSelection } from '../actions'
import view from '../plugin'
import { focusStore, selectionStore } from '../selection'
import ActionContext from './ActionContext.svelte'
import ListView from './ListView.svelte'
import ObjectPresenter from './ObjectPresenter.svelte'
export let viewContext: ViewContext
let search: string = ''
let actions: WithLookup<Action>[] = []
let input: HTMLInputElement
const query = createQuery()
query.query(
view.class.Action,
{},
(res) => {
actions = res
},
{
lookup: {
category: view.class.ActionCategory
}
}
)
const targetQuery = createQuery()
targetQuery.query(view.class.Action, {}, (res) => {
actions = res
})
let supportedActions: WithLookup<Action>[] = []
let filteredActions: WithLookup<Action>[] = []
const client = getClient()
$: {
let fActions: WithLookup<Action>[] = actions
const docs = getSelection($focusStore, $selectionStore)
for (const d of docs) {
fActions = filterActions(client, d, fActions)
}
if (docs.length === 0) {
fActions = fActions.filter((it) => it.input === 'none')
}
fActions = fActions.filter(
(it) =>
(it.$lookup?.category?.visible ?? true) &&
(it.context.application === viewContext.application || it.context.application === undefined)
)
// Sort by category.
supportedActions = fActions.sort((a, b) => a.category.localeCompare(b.category))
}
async function filterSearchActions (actions: WithLookup<Action>[], search: string): Promise<void> {
const res: WithLookup<Action>[] = []
search = search.trim().toLowerCase()
if (search.length > 0) {
for (const a of actions) {
const tr = await translate(a.label, {})
if (tr.toLowerCase().indexOf(search) !== -1) {
res.push(a)
}
}
filteredActions = res
} else {
filteredActions = actions
}
}
$: filterSearchActions(supportedActions, search)
let phTraslate: string = ''
$: translate(view.string.ActionPlaceholder, {}).then((res) => {
phTraslate = res
})
onMount(() => {
if (input) input.focus()
})
let selection = 0
let list: ListView
/* eslint-disable no-undef */
async function handleSelection (evt: Event, selection: number): Promise<void> {
const action = filteredActions[selection]
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)
}
}
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)
}
}
}
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()
handleSelection(key, selection)
}
if (key.code === 'Escape') {
key.preventDefault()
key.stopPropagation()
closePopup()
}
}
function formatKey (key: string): string[][] {
const thens = key.split('->')
const result: string[][] = []
for (const r of thens) {
result.push(
r.split('+').map((it) =>
it
.replaceAll('key', '')
.replaceAll(/Meta|meta/g, '⌘')
.replaceAll('ArrowUp', '↑')
.replaceAll('ArrowDown', '↓')
.replaceAll('ArrowLeft', '←')
.replaceAll('ArrowRight', '→')
.replaceAll('Backspace', '⌫')
)
)
}
return result
}
</script>
<ActionContext
context={{
mode: 'none'
}}
/>
<div class="selectPopup width-40" style:width="15rem" on:keydown={onKeydown}>
<div class="mt-2 ml-2">
{#if $selectionStore.length > 0}
<div class="item-box">
{$selectionStore.length} items
</div>
{:else if $focusStore.focus !== undefined}
<div class="item-box">
<ObjectPresenter
objectId={$focusStore.focus._id}
_class={$focusStore.focus._class}
value={$focusStore.focus}
props={{ inline: true }}
/>
</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" class:ml-2={i !== 0}>
{#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" class:ml-2={j !== 0}>
{kk}
</div>
{/each}
{/each}
</div>
{/each}
{/if}
</div>
</div>
</svelte:fragment>
</ListView>
</div>
</div>
</div>
<style lang="scss">
.key-box {
background-color: var(--divider-color);
color: var(--caption-color);
min-width: 1.5rem;
padding: 0.25rem;
}
.item-box {
display: inline-block;
background-color: var(--divider-color);
color: var(--caption-color);
border-radius: 0.5rem;
padding: 0.5rem;
}
.category-box {
display: inline-block;
background-color: var(--divider-color);
color: var(--caption-color);
padding: 0.5rem;
}
</style>
@@ -75,6 +75,7 @@
let collectionEditors: { key: KeyedAttribute; editor: AnyComponent }[] = []
let mixins: Mixin<Doc>[] = []
$: if (object) {
parentClass = getParentClass(object._class)
getMixins()
@@ -147,6 +148,7 @@
let mainEditor: AnyComponent
$: if (object) getEditorOrDefault(object._class, object._class)
async function getEditorOrDefault (_class: Ref<Class<Doc>> | undefined, defaultClass: Ref<Class<Doc>>): Promise<void> {
let editor = _class !== undefined ? await getEditor(_class) : undefined
if (editor === undefined) {
@@ -0,0 +1,85 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let selection: number = 0
export let count: number
const refs: HTMLElement[] = []
const dispatch = createEventDispatcher()
let oldSelection = Date.now()
function onRow (item: number): void {
if (updateSelection(item)) {
dispatch('on-select', item)
}
}
function updateSelection (item: number): boolean {
const now = Date.now()
if (now - oldSelection >= 25) {
selection = item
oldSelection = now
return true
}
return false
}
export function select (pos: number): void {
if (pos < 0) {
pos = 0
}
if (pos >= count) {
pos = count - 1
}
const r = refs[pos]
onRow(pos)
if (r !== undefined) {
r?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
}
}
</script>
{#if count}
<div class="list-container flex-col flex-grow" style:overflow={'auto'}>
{#each Array(count) as _, row}
<slot name="category" item={row} />
<div
class:selection={row === selection}
on:mouseover={() => onRow(row)}
on:focus={() => {}}
bind:this={refs[row]}
on:click={() => dispatch('click', row)}
>
<slot name="item" item={row} />
</div>
{/each}
</div>
{/if}
<style lang="scss">
.list-container {
background-color: var(--board-card-bg-color);
border-radius: 0.25rem;
user-select: none;
.selection {
background-color: var(--theme-bg-checked-hover);
}
}
</style>
@@ -27,18 +27,18 @@
const client = getClient()
async function invokeAction (evt: Event, action: ViewAction) {
async function invokeAction (evt: Event, action: ViewAction, props?: Record<string, any>) {
const impl = await getResource(action)
await impl(Array.isArray(object) && object.length === 1 ? object[0] : object, evt)
await impl(Array.isArray(object) && object.length === 1 ? object[0] : object, evt, props)
}
let loaded = 0
getActions(client, object, baseMenuClass, !Array.isArray(object) || object.length === 1).then((result) => {
getActions(client, object, baseMenuClass).then((result) => {
actions = result.map((a) => ({
label: a.label,
icon: a.icon as Asset,
action: async (_: any, evt: Event) => {
invokeAction(evt, a.action)
invokeAction(evt, a.action, a.actionProps)
}
}))
loaded = 1
+3 -1
View File
@@ -37,6 +37,7 @@ import TimestampPresenter from './components/TimestampPresenter.svelte'
import UpDownNavigator from './components/UpDownNavigator.svelte'
import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte'
import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte'
import ActionsPopup from './components/ActionsPopup.svelte'
export { getActions } from './actions'
export { default as ActionContext } from './components/ActionContext.svelte'
@@ -69,6 +70,7 @@ export default async (): Promise<Resources> => ({
HTMLPresenter,
IntlStringPresenter,
GithubPresenter,
YoutubePresenter
YoutubePresenter,
ActionsPopup
}
})
+6 -1
View File
@@ -15,9 +15,13 @@
//
import { IntlString, mergeIds } from '@anticrm/platform'
import { AnyComponent } from '@anticrm/ui'
import view, { viewId } from '@anticrm/view'
export default mergeIds(viewId, view, {
component: {
ActionsPopup: '' as AnyComponent
},
string: {
MoveClass: '' as IntlString,
SelectToMove: '' as IntlString,
@@ -30,6 +34,7 @@ export default mergeIds(viewId, view, {
DeleteObject: '' as IntlString,
DeleteObjectConfirm: '' as IntlString,
Assignees: '' as IntlString,
Labels: '' as IntlString
Labels: '' as IntlString,
ActionPlaceholder: '' as IntlString
}
})
+83 -21
View File
@@ -30,6 +30,7 @@ import type {
import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
import { PopupPosAlignment } from '@anticrm/ui/src/types'
/**
* @public
@@ -116,41 +117,70 @@ export type KeyBinding = string
/**
* @public
*/
export type ViewAction = Resource<(doc: Doc | Doc[] | undefined, evt: Event) => Promise<void>>
export type ViewActionInput = 'focus' | 'selection' | 'any' | 'none'
/**
* @public
*/
export interface Action extends Doc, UXObject {
keyBinding?: KeyBinding[]
action: ViewAction
export type ViewAction<T = Record<string, any>> = Resource<
(doc: Doc | Doc[] | undefined, evt: Event, params?: T) => Promise<void>
>
/**
* @public
*/
export interface ActionCategory extends Doc, UXObject {
// Does category is visible for use in popup.
visible: boolean
}
/**
* @public
*/
export interface Action<T extends Doc = Doc, P = Record<string, any>> extends Doc, UXObject {
// Action implementation details
action: ViewAction<P>
// Action implementation parameters
actionProps?: P
// If specified, action could be used only with one item selected.
// By default it is treated as false
singleInput?: boolean
}
// single - one object is required
// any - one or multiple objects are required
// any - any input is suitable.
input: ViewActionInput
/**
* Define action to 'object' mapping.
* @public
*/
export interface ActionTarget<T extends Doc = Doc> extends Doc {
target: Ref<Class<T>>
action: Ref<Action>
// Focus and/or all selection document should match target class.
target: Ref<Class<Doc>>
// Action is applicable only for objects matching criteria
query?: DocumentQuery<T>
context: ViewContext
// If specified, will be used instead of action from Action.
override?: ViewAction
// If defined, types should be matched to proposed list
inputProps?: Record<string, Ref<Class<Doc>>>
// Kayboard bindings
keyBinding?: KeyBinding[]
// short description for action.
description?: IntlString
// Action category, for UI.
category: Ref<ActionCategory>
// Context action is defined for
context: ViewContext
}
/**
* @public
* context - only for context menu actions.
* workbench - global actions per application or entire workbench.
* browser - actions for list/table/kanban browsing.
* editor - actions for selected editor context.
* context - only for context menu actions.
* panel - for panel based actions.
* popup - for popup based actions, like Close of Popup.
* input - for input based actions, some actions should be available for input controls.
*/
export type ViewContextType = 'context' | 'workbench' | 'browser' | 'editor' | 'panel' | 'popup' | 'context'
export type ViewContextType = 'context' | 'workbench' | 'browser' | 'editor' | 'panel' | 'popup' | 'input' | 'none'
/**
* @public
@@ -272,7 +302,7 @@ const view = plugin(viewId, {
ViewletDescriptor: '' as Ref<Class<ViewletDescriptor>>,
Viewlet: '' as Ref<Class<Viewlet>>,
Action: '' as Ref<Class<Action>>,
ActionTarget: '' as Ref<Class<ActionTarget>>,
ActionCategory: '' as Ref<Class<ActionCategory>>,
LinkPresenter: '' as Ref<Class<LinkPresenter>>
},
viewlet: {
@@ -291,7 +321,39 @@ const view = plugin(viewId, {
Move: '' as Asset,
Archive: '' as Asset,
Statuses: '' as Asset,
Open: '' as Asset
Open: '' as Asset,
ArrowRight: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,
GeneralNavigation: '' as Ref<ActionCategory>,
Navigation: '' as Ref<ActionCategory>,
Editor: '' as Ref<ActionCategory>,
MarkdownFormatting: '' as Ref<ActionCategory>
},
actionImpl: {
UpdateDocument: '' as ViewAction<{
key: string
value: any
ask?: boolean
label?: IntlString
message?: IntlString
}>,
ShowPanel: '' as ViewAction<{
component?: AnyComponent
element?: PopupPosAlignment
rightSection?: AnyComponent
}>,
ShowPopup: '' as ViewAction<{
component: AnyComponent
element?: PopupPosAlignment
_id?: string
_class?: string
_space?: string
value?: string
values?: string
props?: Record<string, any>
}>
}
})
export default view
@@ -22,10 +22,10 @@
import { Avatar, createQuery, setClient } from '@anticrm/presentation'
import {
AnyComponent,
closePanel,
closePopup,
closeTooltip,
Component,
DatePickerPopup,
getCurrentLocation,
location,
Location,
@@ -33,12 +33,12 @@
PanelInstance,
Popup,
showPopup,
DatePickerPopup,
TooltipInstance
} from '@anticrm/ui'
import { ActionContext, ActionHandler } from '@anticrm/view-resources'
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@anticrm/workbench'
import { onDestroy, tick } from 'svelte'
import { doNavigate } from '../utils'
import workbench from '../plugin'
import AccountPopup from './AccountPopup.svelte'
import AppItem from './AppItem.svelte'
@@ -182,36 +182,31 @@
}
visibileNav = true
closePanel()
const loc = getCurrentLocation()
loc.path[1] = app._id
loc.path.length = 2
navigate(loc)
doNavigate([], undefined, {
mode: 'app',
application: app._id
})
}
function selectSpecial (id: string): void {
if (currentSpecial === id) return
closePanel()
const loc = getCurrentLocation()
loc.path[2] = id
loc.path.length = 3
doNavigate([], undefined, {
mode: 'special',
special: id
})
checkOnHide()
navigate(loc)
}
function selectSpace (spaceId?: Ref<Space>, spaceSpecial?: string): void {
if (currentSpace === spaceId && (spaceSpecial === currentSpecial || spaceSpecial === asideId)) return
closePanel()
const loc = getCurrentLocation()
if (spaceId !== undefined) {
loc.path[2] = spaceId
const special = spaceSpecial
if (special !== undefined) {
loc.path[3] = special
}
}
doNavigate([], undefined, {
mode: 'space',
space: spaceId,
spaceSpecial: spaceSpecial
})
checkOnHide()
navigate(loc)
}
function closeAside (): void {
@@ -52,7 +52,7 @@
const addSpace: Action = {
label: model.addSpaceLabel,
icon: IconAdd,
action: async (_id: Ref<Doc>, ev?: Event): Promise<void> => {
action: async (_id: Ref<Doc>): Promise<void> => {
dispatch('open')
showPopup(model.createComponent, {}, 'top')
}
@@ -61,7 +61,7 @@
const browseSpaces: Action = {
label: plugin.string.BrowseSpaces,
icon: IconSearch,
action: async (_id: Ref<Doc>, ev?: Event): Promise<void> => {
action: async (_id: Ref<Doc>): Promise<void> => {
const loc = getCurrentLocation()
loc.path[2] = 'spaceBrowser'
dispatch('open')
@@ -111,7 +111,7 @@
label: act.label,
action: async (evt: Event) => {
const impl = await getResource(act.action)
await impl(space, evt)
await impl(space, evt, act.actionProps)
}
})
}
@@ -74,7 +74,7 @@
label: act.label,
action: async (evt: Event) => {
const impl = await getResource(act.action)
await impl(space, evt)
await impl(space, evt, act.actionProps)
}
})
}
+8 -4
View File
@@ -13,13 +13,14 @@
// limitations under the License.
//
import WorkbenchApp from './components/WorkbenchApp.svelte'
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import { Resources } from '@anticrm/platform'
import Archive from './components/Archive.svelte'
import { Space } from '@anticrm/core'
import { Resources } from '@anticrm/platform'
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import Archive from './components/Archive.svelte'
import SpacePanel from './components/navigator/SpacePanel.svelte'
import SpaceBrowser from './components/SpaceBrowser.svelte'
import WorkbenchApp from './components/WorkbenchApp.svelte'
import { doNavigate } from './utils'
function hasArchiveSpaces (spaces: Space[]): boolean {
return spaces.find((sp) => sp.archived) !== undefined
@@ -35,5 +36,8 @@ export default async (): Promise<Resources> => ({
},
function: {
HasArchiveSpaces: hasArchiveSpaces
},
actionImpl: {
Navigate: doNavigate
}
})
+69 -2
View File
@@ -14,11 +14,13 @@
// limitations under the License.
//
import type { Class, Client, Obj, Ref, Space } from '@anticrm/core'
import type { Class, Client, Doc, Obj, Ref, Space } from '@anticrm/core'
import type { Asset } from '@anticrm/platform'
import { getResource } from '@anticrm/platform'
import { NavigatorModel } from '@anticrm/workbench'
import { Application, NavigatorModel } from '@anticrm/workbench'
import view from '@anticrm/view'
import { closePanel, getCurrentLocation, navigate } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
export function classIcon (client: Client, _class: Ref<Class<Obj>>): Asset | undefined {
return client.getHierarchy().getClass(_class).icon
@@ -43,3 +45,68 @@ export async function getSpaceName (client: Client, space: Space): Promise<strin
return space.name
}
/**
* @public
*/
export async function doNavigate (
doc: Doc | Doc[],
evt: Event | undefined,
props: {
mode: 'app' | 'special' | 'space'
application?: Ref<Application>
special?: string
spaceSpecial?: string
space?: Ref<Space>
// If no space is selected, select first space from list
spaceClass?: Ref<Class<Space>>
}
): Promise<void> {
evt?.preventDefault()
closePanel()
const loc = getCurrentLocation()
const client = getClient()
switch (props.mode) {
case 'app':
loc.path[1] = props.application ?? ''
if (props.special !== undefined) {
loc.path[2] = props.special
loc.path.length = 3
} else {
loc.path.length = 2
}
navigate(loc)
break
case 'special':
if (props.application !== undefined && loc.path[1] !== props.application) {
loc.path[1] = props.application
}
loc.path[2] = props.special ?? ''
loc.path.length = 3
navigate(loc)
break
case 'space': {
if (props.space !== undefined) {
loc.path[2] = props.space
}
if (props.spaceSpecial !== undefined) {
loc.path[3] = props.spaceSpecial
}
if (props.spaceClass !== undefined) {
const ex = await client.findOne(props.spaceClass, { _id: loc.path[2] as Ref<Space> })
if (ex === undefined) {
const r = await client.findOne(props.spaceClass, {})
if (r !== undefined) {
loc.path[3] = r._id
navigate(loc)
}
}
}
loc.path.length = 4
navigate(loc)
break
}
}
}
+2 -1
View File
@@ -28,6 +28,7 @@
"dependencies": {
"@anticrm/core": "~0.6.16",
"@anticrm/platform": "~0.6.6",
"@anticrm/ui": "~0.6.0"
"@anticrm/ui": "~0.6.0",
"@anticrm/view": "~0.6.0"
}
}
+12
View File
@@ -17,6 +17,7 @@ import type { Class, Doc, Mixin, Obj, Ref, Space } from '@anticrm/core'
import type { Asset, IntlString, Metadata, Plugin, Resource } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
import { AnyComponent } from '@anticrm/ui'
import { ViewAction } from '@anticrm/view'
/**
* @public
@@ -113,5 +114,16 @@ export default plugin(workbenchId, {
metadata: {
PlatformTitle: '' as Metadata<string>,
ExcludedApplications: '' as Metadata<Ref<Application>[]>
},
actionImpl: {
Navigate: '' as ViewAction<{
mode: 'app' | 'special' | 'space'
application?: Ref<Application>
special?: string
space?: Ref<Space>
// If no space is selected, select first space from list
spaceClass?: Ref<Class<Space>>
spaceSpecial?: string
}>
}
})