From 8d7e3078e1c3387e55c1eebdf44b4f1a8af3a5bd Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Wed, 4 May 2022 15:08:12 +0700 Subject: [PATCH] Keyboard popup (#1625) Signed-off-by: Andrey Sobolev --- dev/tool/src/mdiff.ts | 3 - models/calendar/src/index.ts | 27 +- models/calendar/src/plugin.ts | 5 +- models/chunter/src/index.ts | 109 +++--- models/chunter/src/plugin.ts | 5 +- models/contact/src/index.ts | 8 +- models/inventory/package.json | 3 +- models/inventory/src/index.ts | 28 +- models/inventory/src/plugin.ts | 5 +- models/recruit/src/index.ts | 192 +++++----- models/recruit/src/plugin.ts | 21 +- models/recruit/src/review.ts | 64 ++-- models/task/src/index.ts | 213 +++++------ models/task/src/plugin.ts | 10 +- models/tracker/src/index.ts | 55 ++- models/tracker/src/plugin.ts | 8 +- models/view/src/index.ts | 333 ++++++++++++------ models/view/src/plugin.ts | 7 +- models/workbench/package.json | 4 +- models/workbench/src/index.ts | 36 +- models/workbench/src/plugin.ts | 4 +- packages/platform/src/platform.ts | 2 +- packages/theme/styles/_layouts.scss | 4 + packages/theme/styles/popups.scss | 4 + packages/ui/src/index.ts | 1 + packages/ui/src/types.ts | 13 +- plugins/recruit-assets/lang/en.json | 18 +- plugins/recruit-assets/lang/ru.json | 9 +- .../src/components/CreateApplication.svelte | 19 +- .../src/components/Vacancies.svelte | 7 +- .../src/components/review/CreateReview.svelte | 2 +- plugins/recruit-resources/src/index.ts | 37 +- plugins/task-assets/lang/en.json | 1 - plugins/task-assets/lang/ru.json | 1 - .../src/components/AssignedTasks.svelte | 5 +- .../src/components/CreateTask.svelte | 136 ------- .../src/components/kanban/KanbanView.svelte | 2 - plugins/task-resources/src/index.ts | 61 +--- plugins/task-resources/src/plugin.ts | 1 - plugins/tracker-assets/lang/en.json | 9 +- plugins/tracker-assets/lang/ru.json | 35 +- plugins/view-assets/assets/icons.svg | 5 + plugins/view-assets/lang/en.json | 19 +- plugins/view-assets/lang/ru.json | 7 +- plugins/view-assets/src/index.ts | 3 +- plugins/view-resources/src/actionImpl.ts | 125 ++++++- plugins/view-resources/src/actions.ts | 111 +++--- .../src/components/ActionContext.svelte | 14 + .../src/components/ActionHandler.svelte | 116 ++++-- .../src/components/ActionsPopup.svelte | 274 ++++++++++++++ .../src/components/EditDoc.svelte | 2 + .../src/components/ListView.svelte | 85 +++++ .../view-resources/src/components/Menu.svelte | 8 +- plugins/view-resources/src/index.ts | 4 +- plugins/view-resources/src/plugin.ts | 7 +- plugins/view/src/index.ts | 104 ++++-- .../src/components/Workbench.svelte | 39 +- .../src/components/navigator/SpacesNav.svelte | 6 +- .../components/navigator/StarredNav.svelte | 2 +- plugins/workbench-resources/src/index.ts | 12 +- plugins/workbench-resources/src/utils.ts | 71 +++- plugins/workbench/package.json | 3 +- plugins/workbench/src/index.ts | 12 + 63 files changed, 1627 insertions(+), 909 deletions(-) delete mode 100644 plugins/task-resources/src/components/CreateTask.svelte create mode 100644 plugins/view-resources/src/components/ActionsPopup.svelte create mode 100644 plugins/view-resources/src/components/ListView.svelte diff --git a/dev/tool/src/mdiff.ts b/dev/tool/src/mdiff.ts index 351844a735..2ab879422d 100644 --- a/dev/tool/src/mdiff.ts +++ b/dev/tool/src/mdiff.ts @@ -94,9 +94,6 @@ function getId (d: Doc): Ref { } else if (d._class === ('workbench:class:Application' as Ref)) { const cr = d as any return ('workbench.app.' + (cr.label as string)) as Ref - } else if (d._class === ('view:class:ActionTarget' as Ref)) { - const cr = d as any - return ((cr.target as string) + '.' + (cr.action as string)) as Ref } else if (d._class === ('server-core:class:Trigger' as Ref)) { const cr = d as any return cr.trigger as string as Ref diff --git a/models/calendar/src/index.ts b/models/calendar/src/index.ts index 45e7bb5832..0f5c767fb9 100644 --- a/models/calendar/src/index.ts +++ b/models/calendar/src/index.ts @@ -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 }) diff --git a/models/calendar/src/plugin.ts b/models/calendar/src/plugin.ts index b8c2679be2..fda073baaf 100644 --- a/models/calendar/src/plugin.ts +++ b/models/calendar/src/plugin.ts @@ -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 }, + category: { + Calendar: '' as Ref + }, actionImpl: { SaveEventReminder: '' as ViewAction }, diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index ce784f9eb3..e57217a4c0 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -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, diff --git a/models/chunter/src/plugin.ts b/models/chunter/src/plugin.ts index 4090188957..f83c0589bf 100644 --- a/models/chunter/src/plugin.ts +++ b/models/chunter/src/plugin.ts @@ -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 + }, string: { ApplicationLabelChunter: '' as IntlString, LeftComment: '' as IntlString, diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index 7423c1493f..b283134187 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -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' diff --git a/models/inventory/package.json b/models/inventory/package.json index 5929059530..fe45f99d18 100644 --- a/models/inventory/package.json +++ b/models/inventory/package.json @@ -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" } } diff --git a/models/inventory/src/index.ts b/models/inventory/src/index.ts index 923c9f5019..ce45442896 100644 --- a/models/inventory/src/index.ts +++ b/models/inventory/src/index.ts @@ -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' diff --git a/models/inventory/src/plugin.ts b/models/inventory/src/plugin.ts index 3713488b1e..178c74355a 100644 --- a/models/inventory/src/plugin.ts +++ b/models/inventory/src/plugin.ts @@ -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 + }, component: { Categories: '' as AnyComponent, Products: '' as AnyComponent, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index eb530bfbda..02316989e0 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -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, + 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, + 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' diff --git a/models/recruit/src/plugin.ts b/models/recruit/src/plugin.ts index c8351e0d88..6efcb3c32a 100644 --- a/models/recruit/src/plugin.ts +++ b/models/recruit/src/plugin.ts @@ -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, - CreateApplication: '' as Ref, - EditVacancy: '' as Ref, - CreateReview: '' as Ref, CreateOpinion: '' as Ref }, actionImpl: { - CreateCandidate: '' as ViewAction, - CreateApplication: '' as ViewAction, - EditVacancy: '' as ViewAction, - CreateReview: '' as ViewAction, CreateOpinion: '' as ViewAction }, + category: { + Recruit: '' as Ref + }, 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<(doc: T, client: Client) => Promise>, diff --git a/models/recruit/src/review.ts b/models/recruit/src/review.ts index 15bceb6646..06fea95bb0 100644 --- a/models/recruit/src/review.ts +++ b/models/recruit/src/review.ts @@ -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 = { lookup: { diff --git a/models/task/src/index.ts b/models/task/src/index.ts index f91b6786f5..8a04c1fedc 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -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>(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'] } diff --git a/models/task/src/plugin.ts b/models/task/src/plugin.ts index 8d5210d39a..5736a3dba2 100644 --- a/models/task/src/plugin.ts +++ b/models/task/src/plugin.ts @@ -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, EditStatuses: '' as Ref, - TodoItemMarkDone: '' as Ref, - TodoItemMarkUnDone: '' as Ref, ArchiveSpace: '' as Ref, UnarchiveSpace: '' as Ref }, actionImpl: { - CreateTask: '' as ViewAction, EditStatuses: '' as ViewAction, TodoItemMarkDone: '' as ViewAction, TodoItemMarkUnDone: '' as ViewAction, ArchiveSpace: '' as ViewAction, UnarchiveSpace: '' as ViewAction }, + category: { + Task: '' as Ref + }, component: { ProjectView: '' as AnyComponent, CreateProject: '' as AnyComponent, - CreateTask: '' as AnyComponent, EditIssue: '' as AnyComponent, TaskPresenter: '' as AnyComponent, KanbanCard: '' as AnyComponent, diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index e85df461f7..9b415e8179 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -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'] + } + }) } diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index a2a7e77351..6ad6fc9057 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -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 diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 65b3f50cbb..c67ead5f2d 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -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> ( builder: Builder, - id: Ref, - label: IntlString, - action: ViewAction, - config?: { - icon?: Asset - keyBinding?: KeyBinding[] - singleInput?: boolean - } + data: Data>, + id?: Ref ): 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, - target: Ref>, - options: { - mode: ViewContextType | ViewContextType[] - application?: Ref - 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 + 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 + input!: ViewActionInput + + target!: Ref> + query!: DocumentQuery + + inputProps!: Record>> + keyBinding!: KeyBinding[] + description!: IntlString + category!: Ref + context!: ViewContext } -@Model(view.class.ActionTarget, core.class.Doc, DOMAIN_MODEL) -export class TActionTarget extends TDoc implements ActionTarget { - target!: Ref> - action!: Ref - query!: DocumentQuery - 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> + +/** + * @public + */ +export function template> (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 diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 70c8325cb9..8b9c31bf50 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -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 } }) diff --git a/models/workbench/package.json b/models/workbench/package.json index 5269da456e..a3a764417a 100644 --- a/models/workbench/package.json +++ b/models/workbench/package.json @@ -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" } } diff --git a/models/workbench/src/index.ts b/models/workbench/src/index.ts index 15ecc901cd..5eb31ee263 100644 --- a/models/workbench/src/index.ts +++ b/models/workbench/src/index.ts @@ -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 + special?: string + space?: Ref + spaceClass?: Ref> + 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 + } + }) +} diff --git a/models/workbench/src/plugin.ts b/models/workbench/src/plugin.ts index 105b73209e..78c337fef1 100644 --- a/models/workbench/src/plugin.ts +++ b/models/workbench/src/plugin.ts @@ -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 }, diff --git a/packages/platform/src/platform.ts b/packages/platform/src/platform.ts index 193e73dc4e..e49e95ce1f 100644 --- a/packages/platform/src/platform.ts +++ b/packages/platform/src/platform.ts @@ -75,7 +75,7 @@ function identify (result: Record, 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) diff --git a/packages/theme/styles/_layouts.scss b/packages/theme/styles/_layouts.scss index 9512045999..f7c659d737 100644 --- a/packages/theme/styles/_layouts.scss +++ b/packages/theme/styles/_layouts.scss @@ -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; } diff --git a/packages/theme/styles/popups.scss b/packages/theme/styles/popups.scss index b47aa27800..566dd112bc 100644 --- a/packages/theme/styles/popups.scss +++ b/packages/theme/styles/popups.scss @@ -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); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index cf2c609d2b..342ca04008 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -29,6 +29,7 @@ export type { AnySvelteComponentWithProps, Location, PopupAlignment, + PopupPositionElement, ButtonKind, ButtonSize } from './types' diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 843e32dacd..13885200c5 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -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' diff --git a/plugins/recruit-assets/lang/en.json b/plugins/recruit-assets/lang/en.json index c742f5f8bd..b7290c025c 100644 --- a/plugins/recruit-assets/lang/en.json +++ b/plugins/recruit-assets/lang/en.json @@ -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", diff --git a/plugins/recruit-assets/lang/ru.json b/plugins/recruit-assets/lang/ru.json index 719edef14c..013daeb4a3 100644 --- a/plugins/recruit-assets/lang/ru.json +++ b/plugins/recruit-assets/lang/ru.json @@ -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": "Пожалуйста выберите кандидата", diff --git a/plugins/recruit-resources/src/components/CreateApplication.svelte b/plugins/recruit-resources/src/components/CreateApplication.svelte index 8b99c1aa7a..0287210ada 100644 --- a/plugins/recruit-resources/src/components/CreateApplication.svelte +++ b/plugins/recruit-resources/src/components/CreateApplication.svelte @@ -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 @@ }} > - {#if !preserveCandidate} - diff --git a/plugins/recruit-resources/src/components/review/CreateReview.svelte b/plugins/recruit-resources/src/components/review/CreateReview.svelte index 6d78c871b0..ed91c83717 100644 --- a/plugins/recruit-resources/src/components/review/CreateReview.svelte +++ b/plugins/recruit-resources/src/components/review/CreateReview.svelte @@ -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 = '' diff --git a/plugins/recruit-resources/src/index.ts b/plugins/recruit-resources/src/index.ts index ec28ff8f7e..c07a5401f5 100644 --- a/plugins/recruit-resources/src/index.ts +++ b/plugins/recruit-resources/src/index.ts @@ -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 { - showPopup(CreateApplication, { candidate: object._id, preserveCandidate: true }) -} - -async function editVacancy (object: Doc): Promise { - showPanel(recruit.component.EditVacancy, object._id, object._class, 'right') -} - async function createOpinion (object: Doc): Promise { showPopup(CreateOpinion, { space: object.space, review: object._id }) } -async function createReview (object: Doc): Promise { - showPopup(CreateReview, { application: object._id, preserveApplication: true }) -} - -async function createCandidate (object: Doc | undefined, evt: Event): Promise { - evt.preventDefault() - showPopup(CreateCandidate, {}) -} - export async function applicantValidator (applicant: Applicant, client: Client): Promise { 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 => ({ actionImpl: { - CreateApplication: createApplication, - EditVacancy: editVacancy, - CreateReview: createReview, - CreateOpinion: createOpinion, - CreateCandidate: createCandidate + CreateOpinion: createOpinion }, validator: { ApplicantValidator: applicantValidator, diff --git a/plugins/task-assets/lang/en.json b/plugins/task-assets/lang/en.json index 8b81c7dd63..8ee0962b49 100644 --- a/plugins/task-assets/lang/en.json +++ b/plugins/task-assets/lang/en.json @@ -20,7 +20,6 @@ "StateTemplateColor": "Color", "KanbanTemplateTitle": "Title", "Rank": "Rank", - "CreateTask": "Create task", "TaskCreateLabel": "Task", "EditStates": "Edit states", "Archive": "Archive", diff --git a/plugins/task-assets/lang/ru.json b/plugins/task-assets/lang/ru.json index 5fa8ec0095..f15a88c45f 100644 --- a/plugins/task-assets/lang/ru.json +++ b/plugins/task-assets/lang/ru.json @@ -20,7 +20,6 @@ "StateTemplateColor": "Цвет", "KanbanTemplateTitle": "Заголовок", "Rank": "Ранк", - "CreateTask": "Создать задачу", "TaskCreateLabel": "Задача", "EditStates": "Редактировать статусы", "Archive": "Архивировать", diff --git a/plugins/task-resources/src/components/AssignedTasks.svelte b/plugins/task-resources/src/components/AssignedTasks.svelte index fb2e14b0dc..5258db760a 100644 --- a/plugins/task-resources/src/components/AssignedTasks.svelte +++ b/plugins/task-resources/src/components/AssignedTasks.svelte @@ -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> = task.class.Task @@ -100,7 +100,7 @@ /> -
diff --git a/plugins/task-resources/src/components/CreateTask.svelte b/plugins/task-resources/src/components/CreateTask.svelte deleted file mode 100644 index 5b1037e3dc..0000000000 --- a/plugins/task-resources/src/components/CreateTask.svelte +++ /dev/null @@ -1,136 +0,0 @@ - - - - 0 && _space != null} - spaceClass={task.class.Project} - spaceLabel={plugin.string.ProjectName} - spacePlaceholder={plugin.string.SelectProject} - bind:space={_space} - on:close={() => { - dispatch('close') - }} -> - - - - - - diff --git a/plugins/task-resources/src/components/kanban/KanbanView.svelte b/plugins/task-resources/src/components/kanban/KanbanView.svelte index 7661ce1dd4..5b9c8fdc9a 100644 --- a/plugins/task-resources/src/components/kanban/KanbanView.svelte +++ b/plugins/task-resources/src/components/kanban/KanbanView.svelte @@ -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) diff --git a/plugins/task-resources/src/index.ts b/plugins/task-resources/src/index.ts index cf30a61cdb..9bd47d4030 100644 --- a/plugins/task-resources/src/index.ts +++ b/plugins/task-resources/src/index.ts @@ -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 { - showPopup(CreateTask, { parent: object._id, space: object.space }) -} - async function editStatuses (object: SpaceWithStates): Promise { showPopup(EditStatuses, { _id: object._id, spaceClass: object._class }, 'right') } -async function toggleDone (value: boolean, object: TodoItem): Promise { - await getClient().update(object, { done: value }) -} - -async function ArchiveSpace (object: SpaceWithStates): Promise { - 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 { - 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 ( _class: Ref>, client: Client, @@ -136,7 +89,6 @@ export type StatesBarPosition = 'start' | 'middle' | 'end' | undefined export default async (): Promise => ({ component: { - CreateTask, CreateProject, TaskPresenter, EditIssue, @@ -158,12 +110,7 @@ export default async (): Promise => ({ 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) diff --git a/plugins/task-resources/src/plugin.ts b/plugins/task-resources/src/plugin.ts index dfd2e46cf6..be6d8d2020 100644 --- a/plugins/task-resources/src/plugin.ts +++ b/plugins/task-resources/src/plugin.ts @@ -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, diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 82be2bb2f9..d880ccdf54 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -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": {} } \ No newline at end of file diff --git a/plugins/tracker-assets/lang/ru.json b/plugins/tracker-assets/lang/ru.json index a36b611b88..1d0a353cb1 100644 --- a/plugins/tracker-assets/lang/ru.json +++ b/plugins/tracker-assets/lang/ru.json @@ -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": {} } diff --git a/plugins/view-assets/assets/icons.svg b/plugins/view-assets/assets/icons.svg index 29306bac3a..2b45b22fd7 100644 --- a/plugins/view-assets/assets/icons.svg +++ b/plugins/view-assets/assets/icons.svg @@ -46,4 +46,9 @@ + + + + + diff --git a/plugins/view-assets/lang/en.json b/plugins/view-assets/lang/en.json index d0b9f5068d..0a5833a2cf 100644 --- a/plugins/view-assets/lang/en.json +++ b/plugins/view-assets/lang/en.json @@ -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" } } diff --git a/plugins/view-assets/lang/ru.json b/plugins/view-assets/lang/ru.json index c489e1e92a..025f298b75 100644 --- a/plugins/view-assets/lang/ru.json +++ b/plugins/view-assets/lang/ru.json @@ -15,6 +15,11 @@ "DeleteObjectConfirm": "Вы действительно хотите удалить {count, plural, =1 {этот обьект} other {эти # обьекта}}?", "Open": "Открыть", "Assignees": "Исполнители", - "Labels": "Метки" + "Labels": "Метки", + "ActionPlaceholder": "введите для филтрации...", + "General": "Общее", + "Navigation": "Навигация", + "Editor": "Редактор", + "MarkdownFormatting": "Форматирование" } } diff --git a/plugins/view-assets/src/index.ts b/plugins/view-assets/src/index.ts index 1c9ad379ce..4f737d7cfe 100644 --- a/plugins/view-assets/src/index.ts +++ b/plugins/view-assets/src/index.ts @@ -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`)) diff --git a/plugins/view-resources/src/actionImpl.ts b/plugins/view-resources/src/actionImpl.ts index 8d426e48b4..6321f346fa 100644 --- a/plugins/view-resources/src/actionImpl.ts +++ b/plugins/view-resources/src/actionImpl.ts @@ -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 { } 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 + } +): 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): void { + async function update (): Promise { + 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 } diff --git a/plugins/view-resources/src/actions.ts b/plugins/view-resources/src/actions.ts index c5e0e22bec..fcd6c4300a 100644 --- a/plugins/view-resources/src/actions.ts +++ b/plugins/view-resources/src/actions.ts @@ -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> = core.class.Doc, - singleInput = false -): Promise> { - const targets = await client.findAll(view.class.ActionTarget, { - 'context.mode': 'context' + mode: ViewContextType = 'context' +): Promise { + 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 = { 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>, - context: ViewContext, - multiple: boolean -): Promise> { - 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 + } +): Promise { + 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>, derived: Ref> = core.class.Doc -): Array> { - const result: Array> = [] +): Array> { + const result: Array> = [] 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 diff --git a/plugins/view-resources/src/components/ActionContext.svelte b/plugins/view-resources/src/components/ActionContext.svelte index f3f52b442e..29a5e12d50 100644 --- a/plugins/view-resources/src/components/ActionContext.svelte +++ b/plugins/view-resources/src/components/ActionContext.svelte @@ -1,3 +1,17 @@ + + + + +
+
+ {#if $selectionStore.length > 0} +
+ {$selectionStore.length} items +
+ {:else if $focusStore.focus !== undefined} +
+ +
+ {/if} +
+
+ +
+
+
+ handleSelection(evt, evt.detail)} + > + + {@const action = filteredActions[item]} + {#if item === 0 || (item > 0 && filteredActions[item - 1].$lookup?.category?.label !== action.$lookup?.category?.label)} + + {#if action.$lookup?.category} +
+
+ {/if} + {/if} +
+ + {@const action = filteredActions[item]} +
+
+ +
+
+
+
+ {#if action.keyBinding} + {#each action.keyBinding as key, i} + {#if i !== 0} +
or
+ {/if} +
+ {#each formatKey(key) as k, jj} + {#if jj !== 0} +
then
+ {/if} + {#each k as kk, j} +
+ {kk} +
+ {/each} + {/each} +
+ {/each} + {/if} +
+
+
+
+
+
+
+ + diff --git a/plugins/view-resources/src/components/EditDoc.svelte b/plugins/view-resources/src/components/EditDoc.svelte index 021e0a48a5..8eeaa0db49 100644 --- a/plugins/view-resources/src/components/EditDoc.svelte +++ b/plugins/view-resources/src/components/EditDoc.svelte @@ -75,6 +75,7 @@ let collectionEditors: { key: KeyedAttribute; editor: AnyComponent }[] = [] let mixins: Mixin[] = [] + $: 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> | undefined, defaultClass: Ref>): Promise { let editor = _class !== undefined ? await getEditor(_class) : undefined if (editor === undefined) { diff --git a/plugins/view-resources/src/components/ListView.svelte b/plugins/view-resources/src/components/ListView.svelte new file mode 100644 index 0000000000..a076ea32ec --- /dev/null +++ b/plugins/view-resources/src/components/ListView.svelte @@ -0,0 +1,85 @@ + + + +{#if count} +
+ {#each Array(count) as _, row} + +
onRow(row)} + on:focus={() => {}} + bind:this={refs[row]} + on:click={() => dispatch('click', row)} + > + +
+ {/each} +
+{/if} + + diff --git a/plugins/view-resources/src/components/Menu.svelte b/plugins/view-resources/src/components/Menu.svelte index 26fbf0b032..1f11e53d15 100644 --- a/plugins/view-resources/src/components/Menu.svelte +++ b/plugins/view-resources/src/components/Menu.svelte @@ -27,18 +27,18 @@ const client = getClient() - async function invokeAction (evt: Event, action: ViewAction) { + async function invokeAction (evt: Event, action: ViewAction, props?: Record) { 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 diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 4812748984..20d3b401ac 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -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 => ({ HTMLPresenter, IntlStringPresenter, GithubPresenter, - YoutubePresenter + YoutubePresenter, + ActionsPopup } }) diff --git a/plugins/view-resources/src/plugin.ts b/plugins/view-resources/src/plugin.ts index a517181f28..31bc5a638a 100644 --- a/plugins/view-resources/src/plugin.ts +++ b/plugins/view-resources/src/plugin.ts @@ -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 } }) diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 6731481e60..5c15384fa7 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -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> +export type ViewActionInput = 'focus' | 'selection' | 'any' | 'none' + /** * @public */ -export interface Action extends Doc, UXObject { - keyBinding?: KeyBinding[] - action: ViewAction +export type ViewAction> = Resource< +(doc: Doc | Doc[] | undefined, evt: Event, params?: T) => Promise +> + +/** + * @public + */ +export interface ActionCategory extends Doc, UXObject { + // Does category is visible for use in popup. + visible: boolean +} + +/** + * @public + */ +export interface Action> extends Doc, UXObject { + // Action implementation details + action: ViewAction

+ // 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 extends Doc { - target: Ref> - action: Ref + // Focus and/or all selection document should match target class. + target: Ref> + // Action is applicable only for objects matching criteria query?: DocumentQuery - 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>> + + // Kayboard bindings + keyBinding?: KeyBinding[] + + // short description for action. + description?: IntlString + + // Action category, for UI. + category: Ref + + // 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>, Viewlet: '' as Ref>, Action: '' as Ref>, - ActionTarget: '' as Ref>, + ActionCategory: '' as Ref>, LinkPresenter: '' as Ref> }, 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, + GeneralNavigation: '' as Ref, + Navigation: '' as Ref, + Editor: '' as Ref, + MarkdownFormatting: '' as Ref + }, + 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 + }> } }) export default view diff --git a/plugins/workbench-resources/src/components/Workbench.svelte b/plugins/workbench-resources/src/components/Workbench.svelte index cb58a1322d..4e116ee5b3 100644 --- a/plugins/workbench-resources/src/components/Workbench.svelte +++ b/plugins/workbench-resources/src/components/Workbench.svelte @@ -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, 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 { diff --git a/plugins/workbench-resources/src/components/navigator/SpacesNav.svelte b/plugins/workbench-resources/src/components/navigator/SpacesNav.svelte index e2a888d5ab..7a7015aa4b 100644 --- a/plugins/workbench-resources/src/components/navigator/SpacesNav.svelte +++ b/plugins/workbench-resources/src/components/navigator/SpacesNav.svelte @@ -52,7 +52,7 @@ const addSpace: Action = { label: model.addSpaceLabel, icon: IconAdd, - action: async (_id: Ref, ev?: Event): Promise => { + action: async (_id: Ref): Promise => { dispatch('open') showPopup(model.createComponent, {}, 'top') } @@ -61,7 +61,7 @@ const browseSpaces: Action = { label: plugin.string.BrowseSpaces, icon: IconSearch, - action: async (_id: Ref, ev?: Event): Promise => { + action: async (_id: Ref): Promise => { 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) } }) } diff --git a/plugins/workbench-resources/src/components/navigator/StarredNav.svelte b/plugins/workbench-resources/src/components/navigator/StarredNav.svelte index 7c476f4ab0..614fd90ac5 100644 --- a/plugins/workbench-resources/src/components/navigator/StarredNav.svelte +++ b/plugins/workbench-resources/src/components/navigator/StarredNav.svelte @@ -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) } }) } diff --git a/plugins/workbench-resources/src/index.ts b/plugins/workbench-resources/src/index.ts index 7f4b3613a9..c5f45f5376 100644 --- a/plugins/workbench-resources/src/index.ts +++ b/plugins/workbench-resources/src/index.ts @@ -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 => ({ }, function: { HasArchiveSpaces: hasArchiveSpaces + }, + actionImpl: { + Navigate: doNavigate } }) diff --git a/plugins/workbench-resources/src/utils.ts b/plugins/workbench-resources/src/utils.ts index dd055efce9..6818267ad9 100644 --- a/plugins/workbench-resources/src/utils.ts +++ b/plugins/workbench-resources/src/utils.ts @@ -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>): Asset | undefined { return client.getHierarchy().getClass(_class).icon @@ -43,3 +45,68 @@ export async function getSpaceName (client: Client, space: Space): Promise + special?: string + spaceSpecial?: string + space?: Ref + // If no space is selected, select first space from list + spaceClass?: Ref> + } +): Promise { + 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 }) + 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 + } + } +} diff --git a/plugins/workbench/package.json b/plugins/workbench/package.json index 5c64fe34dc..4dd1ca7ea6 100644 --- a/plugins/workbench/package.json +++ b/plugins/workbench/package.json @@ -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" } } diff --git a/plugins/workbench/src/index.ts b/plugins/workbench/src/index.ts index 0f290ed5d0..273a687045 100644 --- a/plugins/workbench/src/index.ts +++ b/plugins/workbench/src/index.ts @@ -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, ExcludedApplications: '' as Metadata[]> + }, + actionImpl: { + Navigate: '' as ViewAction<{ + mode: 'app' | 'special' | 'space' + application?: Ref + special?: string + space?: Ref + // If no space is selected, select first space from list + spaceClass?: Ref> + spaceSpecial?: string + }> } })