mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-03 00:43:59 +03:00
Vacancy improvements (#2268)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
e83718f45b
commit
0f3ce39cae
@ -364,6 +364,10 @@ export function createModel (builder: Builder): void {
|
|||||||
presenter: contact.component.PersonPresenter
|
presenter: contact.component.PersonPresenter
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(core.class.Account, core.class.Class, view.mixin.ArrayEditor, {
|
||||||
|
inlineEditor: contact.component.AccountArrayEditor
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(core.class.Account, core.class.Class, view.mixin.AttributePresenter, {
|
builder.mixin(core.class.Account, core.class.Class, view.mixin.AttributePresenter, {
|
||||||
presenter: contact.component.EmployeeAccountPresenter
|
presenter: contact.component.EmployeeAccountPresenter
|
||||||
})
|
})
|
||||||
@ -388,9 +392,13 @@ export function createModel (builder: Builder): void {
|
|||||||
filters: ['_class', 'city', 'modifiedOn']
|
filters: ['_class', 'city', 'modifiedOn']
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin(contact.class.Contact, core.class.Class, setting.mixin.Editable, {})
|
builder.mixin(contact.class.Contact, core.class.Class, setting.mixin.Editable, {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(contact.class.Member, core.class.Class, setting.mixin.Editable, {})
|
builder.mixin(contact.class.Member, core.class.Class, setting.mixin.Editable, {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
presentation.class.ObjectSearchCategory,
|
presentation.class.ObjectSearchCategory,
|
||||||
|
@ -41,7 +41,8 @@ export default mergeIds(contactId, contact, {
|
|||||||
EditMember: '' as AnyComponent,
|
EditMember: '' as AnyComponent,
|
||||||
EmployeeArrayEditor: '' as AnyComponent,
|
EmployeeArrayEditor: '' as AnyComponent,
|
||||||
EmployeeEditor: '' as AnyComponent,
|
EmployeeEditor: '' as AnyComponent,
|
||||||
CreateEmployee: '' as AnyComponent
|
CreateEmployee: '' as AnyComponent,
|
||||||
|
AccountArrayEditor: '' as AnyComponent
|
||||||
},
|
},
|
||||||
string: {
|
string: {
|
||||||
Persons: '' as IntlString,
|
Persons: '' as IntlString,
|
||||||
|
@ -21,6 +21,7 @@ export default mergeIds(coreId, core, {
|
|||||||
Private: '' as IntlString,
|
Private: '' as IntlString,
|
||||||
Archived: '' as IntlString,
|
Archived: '' as IntlString,
|
||||||
ClassLabel: '' as IntlString,
|
ClassLabel: '' as IntlString,
|
||||||
ClassPropertyLabel: '' as IntlString
|
ClassPropertyLabel: '' as IntlString,
|
||||||
|
Members: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { Account, AccountRole, Arr, Domain, DOMAIN_MODEL, IndexKind, Ref, Space } from '@anticrm/core'
|
import { Account, AccountRole, Arr, Domain, DOMAIN_MODEL, IndexKind, Ref, Space } from '@anticrm/core'
|
||||||
import { Index, Model, Prop, TypeBoolean, TypeString, UX } from '@anticrm/model'
|
import { ArrOf, Index, Model, Prop, TypeBoolean, TypeRef, TypeString, UX } from '@anticrm/model'
|
||||||
import core from './component'
|
import core from './component'
|
||||||
import { TDoc } from './core'
|
import { TDoc } from './core'
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ export class TSpace extends TDoc implements Space {
|
|||||||
@Prop(TypeBoolean(), core.string.Archived)
|
@Prop(TypeBoolean(), core.string.Archived)
|
||||||
archived!: boolean
|
archived!: boolean
|
||||||
|
|
||||||
|
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
|
||||||
members!: Arr<Ref<Account>>
|
members!: Arr<Ref<Account>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,9 @@ export function createModel (builder: Builder): void {
|
|||||||
editor: inventory.component.EditProduct
|
editor: inventory.component.EditProduct
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin(inventory.class.Product, core.class.Class, setting.mixin.Editable, {})
|
builder.mixin(inventory.class.Product, core.class.Class, setting.mixin.Editable, {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
view.class.Viewlet,
|
view.class.Viewlet,
|
||||||
|
@ -93,7 +93,9 @@ export function createModel (builder: Builder): void {
|
|||||||
editor: lead.component.EditFunnel
|
editor: lead.component.EditFunnel
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin(lead.class.Lead, core.class.Class, setting.mixin.Editable, {})
|
builder.mixin(lead.class.Lead, core.class.Class, setting.mixin.Editable, {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
workbench.class.Application,
|
workbench.class.Application,
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import type { Employee, Organization } from '@anticrm/contact'
|
import type { Employee, Organization } from '@anticrm/contact'
|
||||||
import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@anticrm/core'
|
import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@anticrm/core'
|
||||||
import {
|
import {
|
||||||
|
ArrOf,
|
||||||
Builder,
|
Builder,
|
||||||
Collection,
|
Collection,
|
||||||
Index,
|
Index,
|
||||||
@ -32,17 +33,17 @@ import {
|
|||||||
import attachment from '@anticrm/model-attachment'
|
import attachment from '@anticrm/model-attachment'
|
||||||
import calendar from '@anticrm/model-calendar'
|
import calendar from '@anticrm/model-calendar'
|
||||||
import chunter from '@anticrm/model-chunter'
|
import chunter from '@anticrm/model-chunter'
|
||||||
import contact, { TPerson } from '@anticrm/model-contact'
|
import contact, { TOrganization, TPerson } from '@anticrm/model-contact'
|
||||||
import core, { TSpace } from '@anticrm/model-core'
|
import core, { TSpace } from '@anticrm/model-core'
|
||||||
import presentation from '@anticrm/model-presentation'
|
import presentation from '@anticrm/model-presentation'
|
||||||
import tags from '@anticrm/model-tags'
|
import tags from '@anticrm/model-tags'
|
||||||
import task, { TSpaceWithStates, TTask, actionTemplates } from '@anticrm/model-task'
|
import task, { actionTemplates, TSpaceWithStates, TTask } from '@anticrm/model-task'
|
||||||
import view, { createAction, actionTemplates as viewTemplates } from '@anticrm/model-view'
|
import view, { actionTemplates as viewTemplates, createAction } from '@anticrm/model-view'
|
||||||
import workbench, { Application, createNavigateAction } from '@anticrm/model-workbench'
|
import workbench, { Application, createNavigateAction } from '@anticrm/model-workbench'
|
||||||
import { IntlString } from '@anticrm/platform'
|
import { IntlString } from '@anticrm/platform'
|
||||||
import { Applicant, Candidate, Candidates, recruitId, Vacancy } from '@anticrm/recruit'
|
import { Applicant, Candidate, Candidates, recruitId, Vacancy, VacancyList } from '@anticrm/recruit'
|
||||||
import { KeyBinding } from '@anticrm/view'
|
|
||||||
import setting from '@anticrm/setting'
|
import setting from '@anticrm/setting'
|
||||||
|
import { KeyBinding } from '@anticrm/view'
|
||||||
import recruit from './plugin'
|
import recruit from './plugin'
|
||||||
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
|
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
|
||||||
import { TOpinion, TReview } from './review-model'
|
import { TOpinion, TReview } from './review-model'
|
||||||
@ -69,6 +70,9 @@ export class TVacancy extends TSpaceWithStates implements Vacancy {
|
|||||||
|
|
||||||
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
|
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
|
||||||
comments?: number
|
comments?: number
|
||||||
|
|
||||||
|
@Prop(Collection(chunter.class.Backlink), chunter.string.Comments)
|
||||||
|
relations!: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@Model(recruit.class.Candidates, core.class.Space)
|
@Model(recruit.class.Candidates, core.class.Space)
|
||||||
@ -102,6 +106,13 @@ export class TCandidate extends TPerson implements Candidate {
|
|||||||
reviews?: number
|
reviews?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mixin(recruit.mixin.VacancyList, contact.class.Organization)
|
||||||
|
@UX(recruit.string.VacancyList, recruit.icon.RecruitApplication, undefined, 'name')
|
||||||
|
export class TVacancyList extends TOrganization implements VacancyList {
|
||||||
|
@Prop(ArrOf(TypeRef(recruit.class.Vacancy)), recruit.string.Vacancies)
|
||||||
|
vacancies!: number
|
||||||
|
}
|
||||||
|
|
||||||
@Model(recruit.class.Applicant, task.class.Task)
|
@Model(recruit.class.Applicant, task.class.Task)
|
||||||
@UX(recruit.string.Application, recruit.icon.Application, recruit.string.ApplicationShort, 'number')
|
@UX(recruit.string.Application, recruit.icon.Application, recruit.string.ApplicationShort, 'number')
|
||||||
export class TApplicant extends TTask implements Applicant {
|
export class TApplicant extends TTask implements Applicant {
|
||||||
@ -126,7 +137,7 @@ export class TApplicant extends TTask implements Applicant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
export function createModel (builder: Builder): void {
|
||||||
builder.createModel(TVacancy, TCandidates, TCandidate, TApplicant, TReview, TOpinion)
|
builder.createModel(TVacancy, TCandidates, TCandidate, TApplicant, TReview, TOpinion, TVacancyList)
|
||||||
|
|
||||||
builder.mixin(recruit.class.Vacancy, core.class.Class, workbench.mixin.SpaceView, {
|
builder.mixin(recruit.class.Vacancy, core.class.Class, workbench.mixin.SpaceView, {
|
||||||
view: {
|
view: {
|
||||||
@ -140,13 +151,24 @@ export function createModel (builder: Builder): void {
|
|||||||
editor: recruit.component.Applications
|
editor: recruit.component.Applications
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ArrayEditor, {
|
||||||
|
editor: recruit.component.VacancyList
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(recruit.mixin.Candidate, core.class.Mixin, view.mixin.ObjectFactory, {
|
builder.mixin(recruit.mixin.Candidate, core.class.Mixin, view.mixin.ObjectFactory, {
|
||||||
component: recruit.component.CreateCandidate
|
component: recruit.component.CreateCandidate
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin(recruit.class.Applicant, core.class.Class, setting.mixin.Editable, {})
|
builder.mixin(recruit.class.Applicant, core.class.Class, setting.mixin.Editable, {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(recruit.class.Vacancy, core.class.Class, setting.mixin.Editable, {})
|
builder.mixin(recruit.class.Vacancy, core.class.Class, setting.mixin.Editable, {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
builder.mixin(recruit.mixin.VacancyList, core.class.Class, setting.mixin.Editable, {
|
||||||
|
value: false
|
||||||
|
})
|
||||||
|
|
||||||
const vacanciesId = 'vacancies'
|
const vacanciesId = 'vacancies'
|
||||||
const talentsId = 'talents'
|
const talentsId = 'talents'
|
||||||
|
@ -13,11 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import { Organization } from '@anticrm/contact'
|
||||||
import core, { Doc, Ref, Space, TxOperations } from '@anticrm/core'
|
import core, { Doc, Ref, Space, TxOperations } from '@anticrm/core'
|
||||||
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
|
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
|
||||||
import { DOMAIN_CALENDAR } from '@anticrm/model-calendar'
|
import { DOMAIN_CALENDAR } from '@anticrm/model-calendar'
|
||||||
|
import contact, { DOMAIN_CONTACT } from '@anticrm/model-contact'
|
||||||
|
import { DOMAIN_SPACE } from '@anticrm/model-core'
|
||||||
import tags, { TagCategory } from '@anticrm/model-tags'
|
import tags, { TagCategory } from '@anticrm/model-tags'
|
||||||
import { createKanbanTemplate, createSequence } from '@anticrm/model-task'
|
import { createKanbanTemplate, createSequence } from '@anticrm/model-task'
|
||||||
|
import { Vacancy } from '@anticrm/recruit'
|
||||||
import { getCategories } from '@anticrm/skillset'
|
import { getCategories } from '@anticrm/skillset'
|
||||||
import { KanbanTemplate } from '@anticrm/task'
|
import { KanbanTemplate } from '@anticrm/task'
|
||||||
import recruit from './plugin'
|
import recruit from './plugin'
|
||||||
@ -34,6 +38,33 @@ export const recruitOperation: MigrateOperation = {
|
|||||||
space: recruit.space.Reviews
|
space: recruit.space.Reviews
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const vacancies = await client.find<Vacancy>(
|
||||||
|
DOMAIN_SPACE,
|
||||||
|
{ _class: recruit.class.Vacancy, company: { $exists: true } },
|
||||||
|
{ projection: { _id: 1, company: 1 } }
|
||||||
|
)
|
||||||
|
|
||||||
|
const orgIds = Array.from(vacancies.map((it) => it.company))
|
||||||
|
.filter((it) => it != null)
|
||||||
|
.filter((it, idx, arr) => arr.indexOf(it) === idx) as Ref<Organization>[]
|
||||||
|
const orgs = await client.find<Organization>(DOMAIN_CONTACT, {
|
||||||
|
_class: contact.class.Organization,
|
||||||
|
_id: { $in: orgIds }
|
||||||
|
})
|
||||||
|
for (const o of orgs) {
|
||||||
|
if ((o as any)[recruit.mixin.VacancyList] === undefined) {
|
||||||
|
await client.update(
|
||||||
|
DOMAIN_CONTACT,
|
||||||
|
{ _id: o._id },
|
||||||
|
{
|
||||||
|
[recruit.mixin.VacancyList]: {
|
||||||
|
vacancies: vacancies.filter((it) => it.company === o._id).reduce((a) => a + 1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||||
const tx = new TxOperations(client, core.account.System)
|
const tx = new TxOperations(client, core.account.System)
|
||||||
|
@ -56,7 +56,8 @@ export default mergeIds(recruitId, recruit, {
|
|||||||
GotoSkills: '' as IntlString,
|
GotoSkills: '' as IntlString,
|
||||||
GotoAssigned: '' as IntlString,
|
GotoAssigned: '' as IntlString,
|
||||||
GotoApplicants: '' as IntlString,
|
GotoApplicants: '' as IntlString,
|
||||||
GotoRecruitApplication: '' as IntlString
|
GotoRecruitApplication: '' as IntlString,
|
||||||
|
VacancyList: '' as IntlString
|
||||||
},
|
},
|
||||||
validator: {
|
validator: {
|
||||||
ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
|
ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
|
||||||
@ -81,7 +82,8 @@ export default mergeIds(recruitId, recruit, {
|
|||||||
Opinions: '' as AnyComponent,
|
Opinions: '' as AnyComponent,
|
||||||
OpinionPresenter: '' as AnyComponent,
|
OpinionPresenter: '' as AnyComponent,
|
||||||
NewCandidateHeader: '' as AnyComponent,
|
NewCandidateHeader: '' as AnyComponent,
|
||||||
ApplicantFilter: '' as AnyComponent
|
ApplicantFilter: '' as AnyComponent,
|
||||||
|
VacancyList: '' as AnyComponent
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
DefaultVacancy: '' as Ref<KanbanTemplate>,
|
DefaultVacancy: '' as Ref<KanbanTemplate>,
|
||||||
|
@ -19,6 +19,7 @@ import core from '@anticrm/core'
|
|||||||
import recruit from '@anticrm/recruit'
|
import recruit from '@anticrm/recruit'
|
||||||
import view from '@anticrm/view'
|
import view from '@anticrm/view'
|
||||||
import serverRecruit from '@anticrm/server-recruit'
|
import serverRecruit from '@anticrm/server-recruit'
|
||||||
|
import serverCore from '@anticrm/server-core'
|
||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
export function createModel (builder: Builder): void {
|
||||||
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.HTMLPresenter, {
|
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.HTMLPresenter, {
|
||||||
@ -36,4 +37,8 @@ export function createModel (builder: Builder): void {
|
|||||||
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.TextPresenter, {
|
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.TextPresenter, {
|
||||||
presenter: serverRecruit.function.VacancyTextPresenter
|
presenter: serverRecruit.function.VacancyTextPresenter
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||||
|
trigger: serverRecruit.trigger.OnVacancyUpdate
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,9 @@ export class TIntegrationType extends TDoc implements IntegrationType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Mixin(setting.mixin.Editable, core.class.Class)
|
@Mixin(setting.mixin.Editable, core.class.Class)
|
||||||
export class TEditable extends TClass implements Editable {}
|
export class TEditable extends TClass implements Editable {
|
||||||
|
value!: boolean
|
||||||
|
}
|
||||||
|
|
||||||
@Mixin(setting.mixin.UserMixin, core.class.Class)
|
@Mixin(setting.mixin.UserMixin, core.class.Class)
|
||||||
export class TUserMixin extends TClass implements UserMixin {}
|
export class TUserMixin extends TClass implements UserMixin {}
|
||||||
@ -332,6 +334,8 @@ export function createModel (builder: Builder): void {
|
|||||||
},
|
},
|
||||||
setting.action.DeleteMixin
|
setting.action.DeleteMixin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// builder.mixin(core.class.Space, core.class.Class, setting.mixin.Editable, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { settingOperation } from './migration'
|
export { settingOperation } from './migration'
|
||||||
|
@ -567,7 +567,9 @@ export function createModel (builder: Builder): void {
|
|||||||
presenter: tracker.component.SprintTitlePresenter
|
presenter: tracker.component.SprintTitlePresenter
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.mixin(tracker.class.Issue, core.class.Class, setting.mixin.Editable, {})
|
builder.mixin(tracker.class.Issue, core.class.Class, setting.mixin.Editable, {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(tracker.class.TypeProjectStatus, core.class.Class, view.mixin.AttributeEditor, {
|
builder.mixin(tracker.class.TypeProjectStatus, core.class.Class, view.mixin.AttributeEditor, {
|
||||||
inlineEditor: tracker.component.ProjectStatusEditor
|
inlineEditor: tracker.component.ProjectStatusEditor
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"Collection": "Collection",
|
"Collection": "Collection",
|
||||||
"Array": "Array",
|
"Array": "Array",
|
||||||
"Bag": "Bag",
|
"Bag": "Bag",
|
||||||
"Enum": "Enum"
|
"Enum": "Enum",
|
||||||
|
"Members": "Members"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"Collection": "Коллекция",
|
"Collection": "Коллекция",
|
||||||
"Array": "Массив",
|
"Array": "Массив",
|
||||||
"Bag": "Bag",
|
"Bag": "Bag",
|
||||||
"Enum": "Справочник"
|
"Enum": "Справочник",
|
||||||
|
"Members": "Участники"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,7 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import type { IntlString, Asset } from '@anticrm/platform'
|
import type { Asset, IntlString } from '@anticrm/platform'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"Spaces": "Пространства",
|
"Spaces": "Пространства",
|
||||||
"Unassigned": "Не назначен",
|
"Unassigned": "Не назначен",
|
||||||
"CreateMore": "Создать еще",
|
"CreateMore": "Создать еще",
|
||||||
"NumberMembers": "{count, plural, =0 {нет участников} =1 {1 участник} other {# участника}}",
|
"NumberMembers": "{count, plural, =0 {нет участников} =1 {1 участник} =2 {2 участника} =3 {3 участника} =4 {4 участника} other {# участников}}",
|
||||||
"NumberSpaces": "{count, plural, =0 {В} =1 {В 1 месте} other {В # местах}}",
|
"NumberSpaces": "{count, plural, =0 {В} =1 {В 1 месте} other {В # местах}}",
|
||||||
"InThis": "В этом {space}",
|
"InThis": "В этом {space}",
|
||||||
"NoMatchesInThis": "В этом {space} совпадения не обнаружены",
|
"NoMatchesInThis": "В этом {space} совпадения не обнаружены",
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
export let width: string | undefined = undefined
|
export let width: string | undefined = undefined
|
||||||
export let focusIndex = -1
|
export let focusIndex = -1
|
||||||
export let showTooltip: LabelAndProps | undefined = undefined
|
export let showTooltip: LabelAndProps | undefined = undefined
|
||||||
|
export let showNavigate = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<UserBox
|
<UserBox
|
||||||
@ -56,5 +57,6 @@
|
|||||||
{width}
|
{width}
|
||||||
{focusIndex}
|
{focusIndex}
|
||||||
{showTooltip}
|
{showTooltip}
|
||||||
|
{showNavigate}
|
||||||
on:change
|
on:change
|
||||||
/>
|
/>
|
||||||
|
@ -16,17 +16,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import contact, { Contact, formatName } from '@anticrm/contact'
|
import contact, { Contact, formatName } from '@anticrm/contact'
|
||||||
import type { Class, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
import type { Class, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||||
import type { Asset, IntlString } from '@anticrm/platform'
|
import { Asset, getEmbeddedLabel, IntlString } from '@anticrm/platform'
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
AnySvelteComponent,
|
AnySvelteComponent,
|
||||||
Button,
|
Button,
|
||||||
ButtonKind,
|
ButtonKind,
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
getFocusManager,
|
getFocusManager,
|
||||||
|
Icon,
|
||||||
|
IconOpen,
|
||||||
Label,
|
Label,
|
||||||
|
LabelAndProps,
|
||||||
|
showPanel,
|
||||||
showPopup,
|
showPopup,
|
||||||
LabelAndProps
|
tooltip
|
||||||
} from '@anticrm/ui'
|
} from '@anticrm/ui'
|
||||||
|
import view from '@anticrm/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import presentation from '..'
|
import presentation from '..'
|
||||||
import { ObjectCreate } from '../types'
|
import { ObjectCreate } from '../types'
|
||||||
@ -52,6 +58,7 @@
|
|||||||
export let width: string | undefined = undefined
|
export let width: string | undefined = undefined
|
||||||
export let focusIndex = -1
|
export let focusIndex = -1
|
||||||
export let showTooltip: LabelAndProps | undefined = undefined
|
export let showTooltip: LabelAndProps | undefined = undefined
|
||||||
|
export let showNavigate = true
|
||||||
|
|
||||||
export let create: ObjectCreate | undefined = undefined
|
export let create: ObjectCreate | undefined = undefined
|
||||||
|
|
||||||
@ -110,25 +117,42 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={container} class="min-w-0" class:w-full={width === '100%'}>
|
<div bind:this={container} class="min-w-0" class:w-full={width === '100%'}>
|
||||||
<Button
|
<Button {focusIndex} width={width ?? 'min-content'} {size} {kind} {justify} {showTooltip} on:click={_click}>
|
||||||
{focusIndex}
|
<span slot="content" class="overflow-label flex-grow" class:flex-between={showNavigate && selected}>
|
||||||
icon={hideIcon || selected ? undefined : icon}
|
<div
|
||||||
width={width ?? 'min-content'}
|
class="disabled"
|
||||||
{size}
|
style:width={showNavigate && selected
|
||||||
{kind}
|
? `calc(${width ?? 'min-content'} - 1.5rem)`
|
||||||
{justify}
|
: `${width ?? 'min-content'}`}
|
||||||
{showTooltip}
|
use:tooltip={selected !== undefined ? { label: getEmbeddedLabel(getName(selected)) } : undefined}
|
||||||
on:click={_click}
|
>
|
||||||
>
|
{#if selected}
|
||||||
<span slot="content" class="overflow-label disabled">
|
{#if hideIcon || selected}
|
||||||
{#if selected}
|
<UserInfo value={selected} size={kind === 'link' ? 'x-small' : 'tiny'} {icon} />
|
||||||
{#if hideIcon || selected}
|
{:else}
|
||||||
<UserInfo value={selected} size={kind === 'link' ? 'x-small' : 'tiny'} {icon} />
|
{getName(selected)}
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{getName(selected)}
|
<div class="flex-row-center">
|
||||||
|
{#if icon}
|
||||||
|
<Icon {icon} size={kind === 'link' ? 'small' : size} />
|
||||||
|
{/if}
|
||||||
|
<div class="ml-2">
|
||||||
|
<Label {label} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<Label {label} />
|
{#if selected && showNavigate}
|
||||||
|
<ActionIcon
|
||||||
|
icon={IconOpen}
|
||||||
|
size={'small'}
|
||||||
|
action={() => {
|
||||||
|
if (selected) {
|
||||||
|
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -132,9 +132,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.dialog {
|
&.dialog {
|
||||||
width: 40rem;
|
width: 45rem;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
max-width: 40rem;
|
max-width: 60rem;
|
||||||
max-height: calc(100vh - 2rem);
|
max-height: calc(100vh - 2rem);
|
||||||
|
|
||||||
.antiCard-header {
|
.antiCard-header {
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
class="button {size}"
|
class="button {size}"
|
||||||
use:tooltip={{ label, direction, props: labelProps }}
|
use:tooltip={{ label, direction, props: labelProps }}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:click|stopPropagation={action}
|
on:click|stopPropagation|preventDefault={action}
|
||||||
>
|
>
|
||||||
<div class="icon {size}" class:invisible>
|
<div class="icon {size}" class:invisible>
|
||||||
<Icon {icon} {size} />
|
<Icon {icon} {size} />
|
||||||
|
11
packages/ui/src/components/icons/Open.svelte
Normal file
11
packages/ui/src/components/icons/Open.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let size: 'small' | 'medium' | 'large'
|
||||||
|
const fill: string = 'currentColor'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
|
<path d="M14,13.1H9.2c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5H14c0.3,0,0.5-0.2,0.5-0.5S14.3,13.1,14,13.1z" />
|
||||||
|
<path
|
||||||
|
d="M11.4,7.1C11.4,7.1,11.4,7.1,11.4,7.1c1.2-1.6,1.3-1.6,1.3-1.6C12.9,5,13,4.5,12.9,4c-0.1-0.5-0.4-0.9-0.8-1.2 c0,0-1.1-0.9-1.1-0.9c-0.8-0.7-2.1-0.6-2.8,0.3c0,0,0,0,0,0l-6.3,7.9c-0.3,0.4-0.4,0.9-0.3,1.4l0.5,2.3c0.1,0.2,0.3,0.4,0.5,0.4 c0,0,0,0,0,0l2.4,0c0.5,0,1-0.2,1.3-0.6C8.9,10.2,10.5,8.2,11.4,7.1C11.4,7.1,11.4,7.1,11.4,7.1z M8.9,2.8c0.3-0.4,1-0.5,1.4-0.1 c0,0,1.2,0.9,1.2,0.9c0.2,0.1,0.4,0.3,0.4,0.6c0.1,0.2,0,0.5-0.1,0.7c0,0-0.4,0.5-0.9,1.2L8.1,3.9L8.9,2.8z M5.5,12.9 C5.4,13,5.2,13.1,5,13.1l-2,0l-0.5-1.9c0-0.2,0-0.4,0.1-0.5l4.8-6l2.8,2.2C8.9,8.6,6.8,11.2,5.5,12.9z"
|
||||||
|
/>
|
||||||
|
</svg>
|
@ -132,6 +132,7 @@ export { default as IconDetails } from './components/icons/Details.svelte'
|
|||||||
export { default as IconDetailsFilled } from './components/icons/DetailsFilled.svelte'
|
export { default as IconDetailsFilled } from './components/icons/DetailsFilled.svelte'
|
||||||
export { default as IconScale } from './components/icons/Scale.svelte'
|
export { default as IconScale } from './components/icons/Scale.svelte'
|
||||||
export { default as IconScaleFull } from './components/icons/ScaleFull.svelte'
|
export { default as IconScaleFull } from './components/icons/ScaleFull.svelte'
|
||||||
|
export { default as IconOpen } from './components/icons/Open.svelte'
|
||||||
|
|
||||||
export { default as PanelInstance } from './components/PanelInstance.svelte'
|
export { default as PanelInstance } from './components/PanelInstance.svelte'
|
||||||
export { default as Panel } from './components/Panel.svelte'
|
export { default as Panel } from './components/Panel.svelte'
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import contact, { Employee, EmployeeAccount } from '@anticrm/contact'
|
||||||
|
import core, { Account, Ref } from '@anticrm/core'
|
||||||
|
import { IntlString } from '@anticrm/platform'
|
||||||
|
import { createQuery, getClient, UserBoxList } from '@anticrm/presentation'
|
||||||
|
|
||||||
|
export let label: IntlString
|
||||||
|
export let value: Ref<Account>[]
|
||||||
|
export let onChange: (refs: Ref<Account>[]) => void
|
||||||
|
|
||||||
|
let timer: any
|
||||||
|
const client = getClient()
|
||||||
|
|
||||||
|
function onUpdate (evt: CustomEvent<Ref<Employee>[]>): void {
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(async () => {
|
||||||
|
const accounts = await client.findAll(contact.class.EmployeeAccount, { employee: { $in: evt.detail } })
|
||||||
|
onChange(accounts.map((it) => it._id))
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountQuery = createQuery()
|
||||||
|
|
||||||
|
let accounts: Account[] = []
|
||||||
|
|
||||||
|
$: accountQuery.query(core.class.Account, { _id: { $in: value } }, (res) => {
|
||||||
|
accounts = res
|
||||||
|
})
|
||||||
|
|
||||||
|
$: employess = accounts.map((it) => (it as EmployeeAccount).employee)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<UserBoxList
|
||||||
|
items={employess}
|
||||||
|
{label}
|
||||||
|
on:update={onUpdate}
|
||||||
|
kind={'link'}
|
||||||
|
size={'medium'}
|
||||||
|
justify={'left'}
|
||||||
|
width={'100%'}
|
||||||
|
/>
|
@ -44,6 +44,8 @@
|
|||||||
{size}
|
{size}
|
||||||
{justify}
|
{justify}
|
||||||
{width}
|
{width}
|
||||||
|
allowDeselect
|
||||||
|
titleDeselect={contact.string.Cancel}
|
||||||
bind:value
|
bind:value
|
||||||
on:change={(e) => onChange(e.detail)}
|
on:change={(e) => onChange(e.detail)}
|
||||||
/>
|
/>
|
||||||
|
@ -16,12 +16,9 @@
|
|||||||
import { Organization } from '@anticrm/contact'
|
import { Organization } from '@anticrm/contact'
|
||||||
import { Ref } from '@anticrm/core'
|
import { Ref } from '@anticrm/core'
|
||||||
import { IntlString } from '@anticrm/platform'
|
import { IntlString } from '@anticrm/platform'
|
||||||
import { createQuery } from '@anticrm/presentation'
|
import { UserBox } from '@anticrm/presentation'
|
||||||
import { DropdownPopup, Label, showPopup, Button } from '@anticrm/ui'
|
|
||||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||||
import { ListItem } from '@anticrm/ui/src/types'
|
|
||||||
import contact from '../plugin'
|
import contact from '../plugin'
|
||||||
import Company from './icons/Company.svelte'
|
|
||||||
|
|
||||||
export let value: Ref<Organization> | undefined
|
export let value: Ref<Organization> | undefined
|
||||||
export let label: IntlString = contact.string.Organization
|
export let label: IntlString = contact.string.Organization
|
||||||
@ -29,80 +26,21 @@
|
|||||||
|
|
||||||
export let kind: ButtonKind = 'no-border'
|
export let kind: ButtonKind = 'no-border'
|
||||||
export let size: ButtonSize = 'small'
|
export let size: ButtonSize = 'small'
|
||||||
export let justify: 'left' | 'center' = 'center'
|
export let justify: 'left' | 'center' = 'left'
|
||||||
export let width: string | undefined = 'min-content'
|
export let width: string | undefined = 'min-content'
|
||||||
|
|
||||||
const query = createQuery()
|
|
||||||
|
|
||||||
query.query(contact.class.Organization, {}, (res) => {
|
|
||||||
items = res.map((org) => {
|
|
||||||
return {
|
|
||||||
_id: org._id,
|
|
||||||
label: org.name,
|
|
||||||
image: org.avatar === null ? undefined : org.avatar
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (value !== undefined) {
|
|
||||||
selected = items.find((p) => p._id === value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let items: ListItem[] = []
|
|
||||||
let selected: ListItem | undefined
|
|
||||||
|
|
||||||
function setValue (res: ListItem | undefined): void {
|
|
||||||
selected = res
|
|
||||||
if (selected === undefined) {
|
|
||||||
value = undefined
|
|
||||||
} else {
|
|
||||||
value = selected._id as Ref<Organization>
|
|
||||||
}
|
|
||||||
onChange(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
let opened: boolean = false
|
|
||||||
const icon = Company
|
|
||||||
let tool: HTMLElement
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="clear-mins" bind:this={tool} />
|
<UserBox
|
||||||
<Button
|
_class={contact.class.Organization}
|
||||||
|
{label}
|
||||||
|
{value}
|
||||||
|
{kind}
|
||||||
|
{size}
|
||||||
{justify}
|
{justify}
|
||||||
{width}
|
{width}
|
||||||
{size}
|
allowDeselect
|
||||||
{kind}
|
titleDeselect={contact.string.Cancel}
|
||||||
on:click={() => {
|
on:change={(evt) => {
|
||||||
if (!opened) {
|
onChange(evt.detail)
|
||||||
opened = true
|
|
||||||
showPopup(DropdownPopup, { title: label, items, icon }, tool, (result) => {
|
|
||||||
if (result) setValue(result)
|
|
||||||
opened = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<svelte:fragment slot="content">
|
|
||||||
{#if selected}
|
|
||||||
<div class="flex-row-center pointer-events-none">
|
|
||||||
<div class="icon"><Company size={'small'} /></div>
|
|
||||||
<span class="overflow-label">{selected.label}</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<span class="overflow-label disabled"><Label {label} /></span>
|
|
||||||
{/if}
|
|
||||||
</svelte:fragment>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.icon {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
padding: 0.25rem;
|
|
||||||
color: var(--accent-color);
|
|
||||||
background-color: var(--avatar-bg-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--caption-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
<!--
|
|
||||||
// Copyright © 2022 Hardcore Engineering Inc.
|
|
||||||
//
|
|
||||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License. You may
|
|
||||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
//
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import { Organization } from '@anticrm/contact'
|
|
||||||
import { Ref } from '@anticrm/core'
|
|
||||||
import { IntlString } from '@anticrm/platform'
|
|
||||||
import { createQuery } from '@anticrm/presentation'
|
|
||||||
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
|
|
||||||
import { Dropdown } from '@anticrm/ui'
|
|
||||||
import { ListItem } from '@anticrm/ui/src/types'
|
|
||||||
import { createEventDispatcher } from 'svelte'
|
|
||||||
import contact from '../plugin'
|
|
||||||
import Company from './icons/Company.svelte'
|
|
||||||
|
|
||||||
export let value: Ref<Organization> | undefined
|
|
||||||
export let label: IntlString = contact.string.Organization
|
|
||||||
|
|
||||||
export let kind: ButtonKind = 'no-border'
|
|
||||||
export let size: ButtonSize = 'small'
|
|
||||||
export let justify: 'left' | 'center' = 'center'
|
|
||||||
export let width: string | undefined = undefined
|
|
||||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
|
||||||
export let focusIndex = -1
|
|
||||||
|
|
||||||
const query = createQuery()
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
let items: ListItem[] = []
|
|
||||||
let selected: ListItem | undefined
|
|
||||||
let resolved = false
|
|
||||||
|
|
||||||
query.query(contact.class.Organization, {}, (res) => {
|
|
||||||
items = res.map((org) => {
|
|
||||||
return {
|
|
||||||
_id: org._id,
|
|
||||||
label: org.name,
|
|
||||||
image: org.avatar === null ? undefined : org.avatar
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (value !== undefined) {
|
|
||||||
selected = items.find((p) => p._id === value)
|
|
||||||
}
|
|
||||||
resolved = true
|
|
||||||
})
|
|
||||||
|
|
||||||
$: if (resolved) {
|
|
||||||
setValue(selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setValue (selected: ListItem | undefined): void {
|
|
||||||
if (selected === undefined) {
|
|
||||||
value = undefined
|
|
||||||
} else {
|
|
||||||
value = selected._id as Ref<Organization>
|
|
||||||
}
|
|
||||||
dispatch('change', value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
{focusIndex}
|
|
||||||
icon={Company}
|
|
||||||
{label}
|
|
||||||
placeholder={label}
|
|
||||||
{items}
|
|
||||||
bind:selected
|
|
||||||
{kind}
|
|
||||||
{size}
|
|
||||||
{justify}
|
|
||||||
{width}
|
|
||||||
{labelDirection}
|
|
||||||
/>
|
|
@ -37,6 +37,7 @@ import EditOrganization from './components/EditOrganization.svelte'
|
|||||||
import EditPerson from './components/EditPerson.svelte'
|
import EditPerson from './components/EditPerson.svelte'
|
||||||
import EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svelte'
|
import EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svelte'
|
||||||
import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte'
|
import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte'
|
||||||
|
import AccountArrayEditor from './components/AccountArrayEditor.svelte'
|
||||||
import EmployeeBrowser from './components/EmployeeBrowser.svelte'
|
import EmployeeBrowser from './components/EmployeeBrowser.svelte'
|
||||||
import EmployeeEditor from './components/EmployeeEditor.svelte'
|
import EmployeeEditor from './components/EmployeeEditor.svelte'
|
||||||
import EmployeePresenter from './components/EmployeePresenter.svelte'
|
import EmployeePresenter from './components/EmployeePresenter.svelte'
|
||||||
@ -44,7 +45,6 @@ import MemberPresenter from './components/MemberPresenter.svelte'
|
|||||||
import Members from './components/Members.svelte'
|
import Members from './components/Members.svelte'
|
||||||
import OrganizationEditor from './components/OrganizationEditor.svelte'
|
import OrganizationEditor from './components/OrganizationEditor.svelte'
|
||||||
import OrganizationPresenter from './components/OrganizationPresenter.svelte'
|
import OrganizationPresenter from './components/OrganizationPresenter.svelte'
|
||||||
import OrganizationSelector from './components/OrganizationSelector.svelte'
|
|
||||||
import PersonEditor from './components/PersonEditor.svelte'
|
import PersonEditor from './components/PersonEditor.svelte'
|
||||||
import PersonPresenter from './components/PersonPresenter.svelte'
|
import PersonPresenter from './components/PersonPresenter.svelte'
|
||||||
import SocialEditor from './components/SocialEditor.svelte'
|
import SocialEditor from './components/SocialEditor.svelte'
|
||||||
@ -55,7 +55,6 @@ export {
|
|||||||
ChannelsEditor,
|
ChannelsEditor,
|
||||||
ContactPresenter,
|
ContactPresenter,
|
||||||
ChannelsView,
|
ChannelsView,
|
||||||
OrganizationSelector,
|
|
||||||
ChannelsDropdown,
|
ChannelsDropdown,
|
||||||
EmployeePresenter,
|
EmployeePresenter,
|
||||||
PersonPresenter,
|
PersonPresenter,
|
||||||
@ -144,7 +143,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
EditMember,
|
EditMember,
|
||||||
EmployeeArrayEditor,
|
EmployeeArrayEditor,
|
||||||
EmployeeEditor,
|
EmployeeEditor,
|
||||||
CreateEmployee
|
CreateEmployee,
|
||||||
|
AccountArrayEditor
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
EmployeeQuery: async (
|
EmployeeQuery: async (
|
||||||
|
@ -92,7 +92,9 @@
|
|||||||
"CopyLink": "Copy link",
|
"CopyLink": "Copy link",
|
||||||
"HasActiveApplicant":"Active Only",
|
"HasActiveApplicant":"Active Only",
|
||||||
"HasNoActiveApplicant": "No Active",
|
"HasNoActiveApplicant": "No Active",
|
||||||
"NoneApplications": "None"
|
"NoneApplications": "None",
|
||||||
|
"RelatedIssues": "Related issues",
|
||||||
|
"VacancyList": "Vacancies"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"TalentRequired": "Please select talent",
|
"TalentRequired": "Please select talent",
|
||||||
|
@ -94,7 +94,9 @@
|
|||||||
"CopyLink": "Копировать ссылку",
|
"CopyLink": "Копировать ссылку",
|
||||||
"HasActiveApplicant":"Только активные",
|
"HasActiveApplicant":"Только активные",
|
||||||
"HasNoActiveApplicant": "Не активные",
|
"HasNoActiveApplicant": "Не активные",
|
||||||
"NoneApplications": "Отсутствуют"
|
"NoneApplications": "Отсутствуют",
|
||||||
|
"RelatedIssues": "Связанные задачи",
|
||||||
|
"VacancyList": "Вакансии"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"TalentRequired": "Пожалуйста выберите таланта",
|
"TalentRequired": "Пожалуйста выберите таланта",
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
"@anticrm/rekoni": "~0.6.0",
|
"@anticrm/rekoni": "~0.6.0",
|
||||||
"@anticrm/notification": "~0.6.0",
|
"@anticrm/notification": "~0.6.0",
|
||||||
"@anticrm/tags": "~0.6.2",
|
"@anticrm/tags": "~0.6.2",
|
||||||
"@anticrm/calendar": "~0.6.0"
|
"@anticrm/calendar": "~0.6.0",
|
||||||
|
"@anticrm/tracker": "~0.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,8 @@
|
|||||||
let name: string = ''
|
let name: string = ''
|
||||||
const description: string = ''
|
const description: string = ''
|
||||||
let templateId: Ref<KanbanTemplate> | undefined
|
let templateId: Ref<KanbanTemplate> | undefined
|
||||||
let company: Ref<Organization> | undefined
|
export let company: Ref<Organization> | undefined
|
||||||
|
export let preserveCompany: boolean = false
|
||||||
|
|
||||||
export function canClose (): boolean {
|
export function canClose (): boolean {
|
||||||
return name === '' && templateId !== undefined
|
return name === '' && templateId !== undefined
|
||||||
@ -87,12 +88,15 @@
|
|||||||
_class={contact.class.Organization}
|
_class={contact.class.Organization}
|
||||||
label={recruit.string.Company}
|
label={recruit.string.Company}
|
||||||
placeholder={recruit.string.Company}
|
placeholder={recruit.string.Company}
|
||||||
|
justify={'left'}
|
||||||
bind:value={company}
|
bind:value={company}
|
||||||
allowDeselect
|
allowDeselect
|
||||||
titleDeselect={recruit.string.UnAssignCompany}
|
titleDeselect={recruit.string.UnAssignCompany}
|
||||||
kind={'no-border'}
|
kind={'no-border'}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
icon={Company}
|
icon={Company}
|
||||||
|
readonly={preserveCompany}
|
||||||
|
showNavigate={false}
|
||||||
create={{ component: contact.component.CreateOrganization, label: contact.string.CreateOrganization }}
|
create={{ component: contact.component.CreateOrganization, label: contact.string.CreateOrganization }}
|
||||||
/>
|
/>
|
||||||
<Component
|
<Component
|
||||||
|
@ -18,10 +18,11 @@
|
|||||||
import type { Ref } from '@anticrm/core'
|
import type { Ref } from '@anticrm/core'
|
||||||
import core from '@anticrm/core'
|
import core from '@anticrm/core'
|
||||||
import { Panel } from '@anticrm/panel'
|
import { Panel } from '@anticrm/panel'
|
||||||
import { createQuery, getClient, MembersBox } from '@anticrm/presentation'
|
import { createQuery, getClient } from '@anticrm/presentation'
|
||||||
import { Vacancy } from '@anticrm/recruit'
|
import { Vacancy } from '@anticrm/recruit'
|
||||||
import { FullDescriptionBox } from '@anticrm/text-editor'
|
import { FullDescriptionBox } from '@anticrm/text-editor'
|
||||||
import { Button, EditBox, Grid, IconMoreH, showPopup } from '@anticrm/ui'
|
import tracker from '@anticrm/tracker'
|
||||||
|
import { Button, Component, EditBox, Grid, Icon, IconAdd, IconMoreH, Label, showPopup } from '@anticrm/ui'
|
||||||
import { ClassAttributeBar, ContextMenu } from '@anticrm/view-resources'
|
import { ClassAttributeBar, ContextMenu } from '@anticrm/view-resources'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import recruit from '../plugin'
|
import recruit from '../plugin'
|
||||||
@ -58,6 +59,7 @@
|
|||||||
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
|
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let isCreateIssue = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if object}
|
{#if object}
|
||||||
@ -79,8 +81,8 @@
|
|||||||
<ClassAttributeBar
|
<ClassAttributeBar
|
||||||
{object}
|
{object}
|
||||||
_class={object._class}
|
_class={object._class}
|
||||||
ignoreKeys={['name', 'description', 'fullDescription']}
|
ignoreKeys={['name', 'description', 'fullDescription', 'private', 'archived']}
|
||||||
to={core.class.Space}
|
to={core.class.Doc}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -130,7 +132,41 @@
|
|||||||
space={object.space}
|
space={object.space}
|
||||||
attachments={object.attachments ?? 0}
|
attachments={object.attachments ?? 0}
|
||||||
/>
|
/>
|
||||||
<MembersBox label={recruit.string.Members} space={object} />
|
<!-- <MembersBox label={recruit.string.Members} space={object} /> -->
|
||||||
</Grid>
|
|
||||||
|
<div class="antiSection">
|
||||||
|
<div class="antiSection-header">
|
||||||
|
<div class="antiSection-header__icon">
|
||||||
|
<Icon icon={tracker.icon.Issue} size={'small'} />
|
||||||
|
</div>
|
||||||
|
<span class="antiSection-header__title">
|
||||||
|
<Label label={recruit.string.RelatedIssues} />
|
||||||
|
</span>
|
||||||
|
<div class="buttons-group small-gap">
|
||||||
|
<Button
|
||||||
|
id="add-sub-issue"
|
||||||
|
width="min-content"
|
||||||
|
icon={IconAdd}
|
||||||
|
label={undefined}
|
||||||
|
labelParams={{ subIssues: 0 }}
|
||||||
|
kind={'transparent'}
|
||||||
|
size={'small'}
|
||||||
|
on:click={() => {
|
||||||
|
isCreateIssue = true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<Component
|
||||||
|
is={tracker.component.RelatedIssues}
|
||||||
|
props={{ object: object, isCreating: isCreateIssue }}
|
||||||
|
on:close={() => {
|
||||||
|
isCreateIssue = false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div></Grid
|
||||||
|
>
|
||||||
</Panel>
|
</Panel>
|
||||||
{/if}
|
{/if}
|
||||||
|
79
plugins/recruit-resources/src/components/VacancyList.svelte
Normal file
79
plugins/recruit-resources/src/components/VacancyList.svelte
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { Doc, Ref } from '@anticrm/core'
|
||||||
|
import { getEmbeddedLabel } from '@anticrm/platform'
|
||||||
|
import presentation from '@anticrm/presentation'
|
||||||
|
import { Button, Icon, IconAdd, Label, showPopup } from '@anticrm/ui'
|
||||||
|
import view, { BuildModelKey } from '@anticrm/view'
|
||||||
|
import { Table } from '@anticrm/view-resources'
|
||||||
|
import recruit from '../plugin'
|
||||||
|
import CreateVacancy from './CreateVacancy.svelte'
|
||||||
|
import FileDuo from './icons/FileDuo.svelte'
|
||||||
|
|
||||||
|
export let objectId: Ref<Doc>
|
||||||
|
export let vacancies: number | undefined
|
||||||
|
|
||||||
|
const createApp = (ev: MouseEvent): void => {
|
||||||
|
showPopup(CreateVacancy, { company: objectId, preserveCompany: true }, ev.target as HTMLElement)
|
||||||
|
}
|
||||||
|
const config: (BuildModelKey | string)[] = [
|
||||||
|
'',
|
||||||
|
'comments',
|
||||||
|
'attachments',
|
||||||
|
{
|
||||||
|
key: 'archived',
|
||||||
|
presenter: view.component.BooleanTruePresenter,
|
||||||
|
label: presentation.string.Archived,
|
||||||
|
props: {
|
||||||
|
useInvert: false,
|
||||||
|
trueColor: 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'modifiedOn'
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="antiSection">
|
||||||
|
<div class="antiSection-header">
|
||||||
|
<div class="antiSection-header__icon">
|
||||||
|
<Icon icon={recruit.icon.Vacancy} size={'small'} />
|
||||||
|
</div>
|
||||||
|
<span class="antiSection-header__title">
|
||||||
|
<Label label={recruit.string.Vacancies} />
|
||||||
|
</span>
|
||||||
|
<Button id="appls.add" icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} />
|
||||||
|
</div>
|
||||||
|
{#if (vacancies ?? 0) > 0}
|
||||||
|
<Table
|
||||||
|
_class={recruit.class.Vacancy}
|
||||||
|
{config}
|
||||||
|
query={{ company: objectId }}
|
||||||
|
loadingProps={{ length: vacancies ?? 0 }}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="antiSection-empty solid flex-col-center mt-3">
|
||||||
|
<div class="caption-color">
|
||||||
|
<FileDuo size={'large'} />
|
||||||
|
</div>
|
||||||
|
<span class="dark-color">
|
||||||
|
<Label label={getEmbeddedLabel('No Vacancies')} />
|
||||||
|
</span>
|
||||||
|
<span class="over-underline content-accent-color" on:click={createApp}>
|
||||||
|
<Label label={recruit.string.CreateVacancy} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
@ -16,7 +16,6 @@
|
|||||||
import calendar from '@anticrm/calendar'
|
import calendar from '@anticrm/calendar'
|
||||||
import type { Contact, EmployeeAccount, Organization, Person } from '@anticrm/contact'
|
import type { Contact, EmployeeAccount, Organization, Person } from '@anticrm/contact'
|
||||||
import contact from '@anticrm/contact'
|
import contact from '@anticrm/contact'
|
||||||
import { OrganizationSelector } from '@anticrm/contact-resources'
|
|
||||||
import { Account, Class, Client, Doc, generateId, getCurrentAccount, Ref } from '@anticrm/core'
|
import { Account, Class, Client, Doc, generateId, getCurrentAccount, Ref } from '@anticrm/core'
|
||||||
import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
|
import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
|
||||||
import { Card, getClient, UserBox, UserBoxList } from '@anticrm/presentation'
|
import { Card, getClient, UserBox, UserBoxList } from '@anticrm/presentation'
|
||||||
@ -171,7 +170,13 @@
|
|||||||
create={{ component: recruit.component.CreateCandidate, label: recruit.string.CreateTalent }}
|
create={{ component: recruit.component.CreateCandidate, label: recruit.string.CreateTalent }}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<OrganizationSelector bind:value={company} label={recruit.string.Company} kind={'no-border'} size={'small'} />
|
<UserBox
|
||||||
|
_class={contact.class.Organization}
|
||||||
|
bind:value={company}
|
||||||
|
label={recruit.string.Company}
|
||||||
|
kind={'no-border'}
|
||||||
|
size={'small'}
|
||||||
|
/>
|
||||||
<DateRangePresenter
|
<DateRangePresenter
|
||||||
bind:value={startDate}
|
bind:value={startDate}
|
||||||
labelNull={recruit.string.StartDate}
|
labelNull={recruit.string.StartDate}
|
||||||
|
@ -46,7 +46,10 @@
|
|||||||
let candidate: Contact | undefined = undefined
|
let candidate: Contact | undefined = undefined
|
||||||
|
|
||||||
async function updateSelected (object: Review) {
|
async function updateSelected (object: Review) {
|
||||||
candidate = await client.findOne<Contact>(object.attachedToClass, { _id: object.attachedTo })
|
candidate =
|
||||||
|
object?.attachedTo !== undefined
|
||||||
|
? await client.findOne<Contact>(object.attachedToClass, { _id: object.attachedTo })
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
$: updateSelected(object)
|
$: updateSelected(object)
|
||||||
|
@ -51,6 +51,7 @@ import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svel
|
|||||||
import VacancyPresenter from './components/VacancyPresenter.svelte'
|
import VacancyPresenter from './components/VacancyPresenter.svelte'
|
||||||
import recruit from './plugin'
|
import recruit from './plugin'
|
||||||
import { copyToClipboard, getApplicationTitle } from './utils'
|
import { copyToClipboard, getApplicationTitle } from './utils'
|
||||||
|
import VacancyList from './components/VacancyList.svelte'
|
||||||
|
|
||||||
async function createOpinion (object: Doc): Promise<void> {
|
async function createOpinion (object: Doc): Promise<void> {
|
||||||
showPopup(CreateOpinion, { space: object.space, review: object._id })
|
showPopup(CreateOpinion, { space: object.space, review: object._id })
|
||||||
@ -287,7 +288,9 @@ export default async (): Promise<Resources> => ({
|
|||||||
|
|
||||||
NewCandidateHeader,
|
NewCandidateHeader,
|
||||||
|
|
||||||
ApplicantFilter
|
ApplicantFilter,
|
||||||
|
|
||||||
|
VacancyList
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
ApplicationQuery: async (
|
ApplicationQuery: async (
|
||||||
|
@ -105,7 +105,8 @@ export default mergeIds(recruitId, recruit, {
|
|||||||
FullDescription: '' as IntlString,
|
FullDescription: '' as IntlString,
|
||||||
HasActiveApplicant: '' as IntlString,
|
HasActiveApplicant: '' as IntlString,
|
||||||
HasNoActiveApplicant: '' as IntlString,
|
HasNoActiveApplicant: '' as IntlString,
|
||||||
NoneApplications: '' as IntlString
|
NoneApplications: '' as IntlString,
|
||||||
|
RelatedIssues: '' as IntlString
|
||||||
},
|
},
|
||||||
space: {
|
space: {
|
||||||
CandidatesPublic: '' as Ref<Space>
|
CandidatesPublic: '' as Ref<Space>
|
||||||
|
@ -32,6 +32,13 @@ export interface Vacancy extends SpaceWithStates {
|
|||||||
company?: Ref<Organization>
|
company?: Ref<Organization>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface VacancyList extends Organization {
|
||||||
|
vacancies: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -105,7 +112,8 @@ const recruit = plugin(recruitId, {
|
|||||||
Opinion: '' as Ref<Class<Opinion>>
|
Opinion: '' as Ref<Class<Opinion>>
|
||||||
},
|
},
|
||||||
mixin: {
|
mixin: {
|
||||||
Candidate: '' as Ref<Mixin<Candidate>>
|
Candidate: '' as Ref<Mixin<Candidate>>,
|
||||||
|
VacancyList: '' as Ref<Mixin<VacancyList>>
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
EditVacancy: '' as AnyComponent
|
EditVacancy: '' as AnyComponent
|
||||||
|
@ -36,7 +36,8 @@
|
|||||||
cls.extends === _class &&
|
cls.extends === _class &&
|
||||||
!cls.hidden &&
|
!cls.hidden &&
|
||||||
[ClassifierKind.CLASS, ClassifierKind.MIXIN].includes(cls.kind) &&
|
[ClassifierKind.CLASS, ClassifierKind.MIXIN].includes(cls.kind) &&
|
||||||
cls.label !== undefined
|
cls.label !== undefined &&
|
||||||
|
(!hierarchy.hasMixin(cls, settings.mixin.Editable) || hierarchy.as(cls, settings.mixin.Editable).value)
|
||||||
) {
|
) {
|
||||||
result.push(clazz)
|
result.push(clazz)
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,12 @@
|
|||||||
let classes: Ref<Class<Doc>>[] = []
|
let classes: Ref<Class<Doc>>[] = []
|
||||||
clQuery.query(core.class.Class, {}, (res) => {
|
clQuery.query(core.class.Class, {}, (res) => {
|
||||||
classes = res
|
classes = res
|
||||||
.filter((p) => hierarchy.hasMixin(p, setting.mixin.Editable) && !hierarchy.hasMixin(p, setting.mixin.UserMixin))
|
.filter(
|
||||||
|
(p) =>
|
||||||
|
hierarchy.hasMixin(p, setting.mixin.Editable) &&
|
||||||
|
hierarchy.as(p, setting.mixin.Editable).value &&
|
||||||
|
!hierarchy.hasMixin(p, setting.mixin.UserMixin)
|
||||||
|
)
|
||||||
.map((p) => p._id)
|
.map((p) => p._id)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -34,7 +34,9 @@
|
|||||||
icon: value.icon
|
icon: value.icon
|
||||||
}
|
}
|
||||||
const id = await client.createDoc(core.class.Mixin, core.space.Model, data)
|
const id = await client.createDoc(core.class.Mixin, core.space.Model, data)
|
||||||
await client.createMixin(id, core.class.Mixin, core.space.Model, setting.mixin.Editable, {})
|
await client.createMixin(id, core.class.Mixin, core.space.Model, setting.mixin.Editable, {
|
||||||
|
value: true
|
||||||
|
})
|
||||||
await client.createMixin(id, core.class.Mixin, core.space.Model, setting.mixin.UserMixin, {})
|
await client.createMixin(id, core.class.Mixin, core.space.Model, setting.mixin.UserMixin, {})
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,9 @@ export interface Integration extends Doc {
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface Editable extends Class<Doc> {}
|
export interface Editable extends Class<Doc> {
|
||||||
|
value: boolean // true is editable, false is not
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
@ -336,58 +336,60 @@
|
|||||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||||
/>
|
/>
|
||||||
<svelte:fragment slot="pool">
|
<svelte:fragment slot="pool">
|
||||||
{#if issueStatuses}
|
<div class="flex flex-wrap" style:gap={'0.2vw'}>
|
||||||
<div id="status-editor">
|
{#if issueStatuses}
|
||||||
<StatusEditor
|
<div id="status-editor">
|
||||||
|
<StatusEditor
|
||||||
|
value={object}
|
||||||
|
statuses={issueStatuses}
|
||||||
|
kind="no-border"
|
||||||
|
size="small"
|
||||||
|
shouldShowLabel={true}
|
||||||
|
on:change={({ detail }) => (object.status = detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PriorityEditor
|
||||||
value={object}
|
value={object}
|
||||||
statuses={issueStatuses}
|
shouldShowLabel
|
||||||
|
isEditable
|
||||||
kind="no-border"
|
kind="no-border"
|
||||||
size="small"
|
size="small"
|
||||||
shouldShowLabel={true}
|
justify="center"
|
||||||
on:change={({ detail }) => (object.status = detail)}
|
on:change={({ detail }) => (object.priority = detail)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<AssigneeEditor
|
||||||
<PriorityEditor
|
value={object}
|
||||||
value={object}
|
size="small"
|
||||||
shouldShowLabel
|
kind="no-border"
|
||||||
isEditable
|
width={'min-content'}
|
||||||
kind="no-border"
|
on:change={({ detail }) => (object.assignee = detail)}
|
||||||
size="small"
|
/>
|
||||||
justify="center"
|
<Component
|
||||||
on:change={({ detail }) => (object.priority = detail)}
|
is={tags.component.TagsDropdownEditor}
|
||||||
/>
|
props={{
|
||||||
<AssigneeEditor
|
items: labels,
|
||||||
value={object}
|
key,
|
||||||
size="small"
|
targetClass: tracker.class.Issue,
|
||||||
kind="no-border"
|
countLabel: tracker.string.NumberLabels
|
||||||
width={'min-content'}
|
}}
|
||||||
on:change={({ detail }) => (object.assignee = detail)}
|
on:open={(evt) => {
|
||||||
/>
|
addTagRef(evt.detail)
|
||||||
<Component
|
}}
|
||||||
is={tags.component.TagsDropdownEditor}
|
on:delete={(evt) => {
|
||||||
props={{
|
labels = labels.filter((it) => it._id !== evt.detail)
|
||||||
items: labels,
|
}}
|
||||||
key,
|
/>
|
||||||
targetClass: tracker.class.Issue,
|
<EstimationEditor kind={'no-border'} size={'small'} value={object} />
|
||||||
countLabel: tracker.string.NumberLabels
|
<ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} />
|
||||||
}}
|
<SprintSelector value={object.sprint} onSprintIdChange={handleSprintIdChanged} />
|
||||||
on:open={(evt) => {
|
{#if object.dueDate !== null}
|
||||||
addTagRef(evt.detail)
|
<DatePresenter bind:value={object.dueDate} editable />
|
||||||
}}
|
{/if}
|
||||||
on:delete={(evt) => {
|
{:else}
|
||||||
labels = labels.filter((it) => it._id !== evt.detail)
|
<Spinner size="small" />
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<EstimationEditor kind={'no-border'} size={'small'} value={object} />
|
|
||||||
<ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} />
|
|
||||||
<SprintSelector value={object.sprint} onSprintIdChange={handleSprintIdChanged} />
|
|
||||||
{#if object.dueDate !== null}
|
|
||||||
<DatePresenter bind:value={object.dueDate} editable />
|
|
||||||
{/if}
|
{/if}
|
||||||
<ActionIcon icon={IconMoreH} size={'medium'} action={showMoreActions} />
|
</div>
|
||||||
{:else}
|
<ActionIcon icon={IconMoreH} size={'medium'} action={showMoreActions} />
|
||||||
<Spinner size="small" />
|
|
||||||
{/if}
|
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<Button
|
<Button
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
{size}
|
{size}
|
||||||
{kind}
|
{kind}
|
||||||
{width}
|
{width}
|
||||||
|
showNavigate={false}
|
||||||
justify={'left'}
|
justify={'left'}
|
||||||
showTooltip={{ label: tracker.string.AssignTo, direction: tooltipAlignment }}
|
showTooltip={{ label: tracker.string.AssignTo, direction: tooltipAlignment }}
|
||||||
on:change={({ detail }) => handleAssigneeChanged(detail)}
|
on:change={({ detail }) => handleAssigneeChanged(detail)}
|
||||||
|
@ -172,11 +172,13 @@
|
|||||||
focus
|
focus
|
||||||
/>
|
/>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<StyledTextArea
|
{#key newIssue.description}
|
||||||
bind:content={newIssue.description}
|
<StyledTextArea
|
||||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
bind:content={newIssue.description}
|
||||||
showButtons={false}
|
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||||
/>
|
showButtons={false}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -275,8 +275,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
{#key issue._id}
|
{#key issue._id && currentTeam !== undefined}
|
||||||
<SubIssues {issue} {issueStatuses} {currentTeam} />
|
{#if currentTeam !== undefined && issueStatuses !== undefined}
|
||||||
|
<SubIssues
|
||||||
|
{issue}
|
||||||
|
issueStatuses={new Map([[currentTeam._id, issueStatuses]])}
|
||||||
|
teams={new Map([[currentTeam?._id, currentTeam]])}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Doc, WithLookup } from '@anticrm/core'
|
import { Doc, Ref, WithLookup } from '@anticrm/core'
|
||||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||||
import { getEventPositionElement, showPanel, showPopup } from '@anticrm/ui'
|
import { getEventPositionElement, showPanel, showPopup } from '@anticrm/ui'
|
||||||
import {
|
import {
|
||||||
@ -35,8 +35,9 @@
|
|||||||
import EstimationEditor from '../timereport/EstimationEditor.svelte'
|
import EstimationEditor from '../timereport/EstimationEditor.svelte'
|
||||||
|
|
||||||
export let issues: Issue[]
|
export let issues: Issue[]
|
||||||
export let issueStatuses: WithLookup<IssueStatus>[]
|
|
||||||
export let currentTeam: Team
|
export let teams: Map<Ref<Team>, Team>
|
||||||
|
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
@ -99,6 +100,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{#each issues as issue, index (issue._id)}
|
{#each issues as issue, index (issue._id)}
|
||||||
|
{@const currentTeam = teams.get(issue.space)}
|
||||||
<div
|
<div
|
||||||
class="flex-between row"
|
class="flex-between row"
|
||||||
class:is-dragging={index === draggingIndex}
|
class:is-dragging={index === draggingIndex}
|
||||||
@ -133,12 +135,14 @@
|
|||||||
justify={'left'}
|
justify={'left'}
|
||||||
on:update={(result) => checkWidth('issue', result)}
|
on:update={(result) => checkWidth('issue', result)}
|
||||||
>
|
>
|
||||||
{getIssueId(currentTeam, issue)}
|
{#if currentTeam}
|
||||||
|
{getIssueId(currentTeam, issue)}
|
||||||
|
{/if}
|
||||||
</FixedColumn>
|
</FixedColumn>
|
||||||
</span>
|
</span>
|
||||||
<StatusEditor
|
<StatusEditor
|
||||||
value={issue}
|
value={issue}
|
||||||
statuses={issueStatuses}
|
statuses={issueStatuses.get(issue.space)}
|
||||||
justify="center"
|
justify="center"
|
||||||
kind={'list'}
|
kind={'list'}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { SortingOrder, WithLookup } from '@anticrm/core'
|
import { Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||||
import { createQuery, getClient } from '@anticrm/presentation'
|
import { createQuery, getClient } from '@anticrm/presentation'
|
||||||
import { calcRank, Issue, IssueStatus, Team } from '@anticrm/tracker'
|
import { calcRank, Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||||
import { Button, Spinner, ExpandCollapse, closeTooltip, IconAdd } from '@anticrm/ui'
|
import { Button, Spinner, ExpandCollapse, closeTooltip, IconAdd } from '@anticrm/ui'
|
||||||
@ -24,8 +24,8 @@
|
|||||||
import SubIssueList from './SubIssueList.svelte'
|
import SubIssueList from './SubIssueList.svelte'
|
||||||
|
|
||||||
export let issue: Issue
|
export let issue: Issue
|
||||||
export let currentTeam: Team | undefined
|
export let teams: Map<Ref<Team>, Team>
|
||||||
export let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||||
|
|
||||||
const subIssuesQuery = createQuery()
|
const subIssuesQuery = createQuery()
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
@ -86,19 +86,28 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
{#if subIssues && issueStatuses && currentTeam}
|
{#if subIssues && issueStatuses}
|
||||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||||
{#if hasSubIssues}
|
{#if hasSubIssues}
|
||||||
<div class="list" class:collapsed={isCollapsed}>
|
<div class="list" class:collapsed={isCollapsed}>
|
||||||
<SubIssueList issues={subIssues} {issueStatuses} {currentTeam} on:move={handleIssueSwap} />
|
<SubIssueList issues={subIssues} {issueStatuses} {teams} on:move={handleIssueSwap} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</ExpandCollapse>
|
</ExpandCollapse>
|
||||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||||
{#if isCreating}
|
{#if isCreating}
|
||||||
<div class="pt-4">
|
{@const team = teams.get(issue.space)}
|
||||||
<CreateSubIssue parentIssue={issue} {issueStatuses} {currentTeam} on:close={() => (isCreating = false)} />
|
{@const statuses = issueStatuses.get(issue.space)}
|
||||||
</div>
|
{#if team !== undefined && statuses !== undefined}
|
||||||
|
<div class="pt-4">
|
||||||
|
<CreateSubIssue
|
||||||
|
parentIssue={issue}
|
||||||
|
issueStatuses={statuses}
|
||||||
|
currentTeam={team}
|
||||||
|
on:close={() => (isCreating = false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</ExpandCollapse>
|
</ExpandCollapse>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -0,0 +1,255 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import core, { Account, AttachedData, Doc, generateId, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||||
|
import presentation, { getClient, KeyedAttribute, SpaceSelector } from '@anticrm/presentation'
|
||||||
|
import { StyledTextArea } from '@anticrm/text-editor'
|
||||||
|
import { IssueStatus, IssuePriority, Issue, Team, calcRank } from '@anticrm/tracker'
|
||||||
|
import { Button, Component, EditBox } from '@anticrm/ui'
|
||||||
|
import tags, { TagElement, TagReference } from '@anticrm/tags'
|
||||||
|
import tracker from '../../../plugin'
|
||||||
|
import AssigneeEditor from '../AssigneeEditor.svelte'
|
||||||
|
import StatusEditor from '../StatusEditor.svelte'
|
||||||
|
import PriorityEditor from '../PriorityEditor.svelte'
|
||||||
|
import EstimationEditor from '../timereport/EstimationEditor.svelte'
|
||||||
|
|
||||||
|
export let related: Doc
|
||||||
|
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||||
|
export let teams: Map<Ref<Team>, Team> | undefined
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const client = getClient()
|
||||||
|
|
||||||
|
let newIssue: AttachedData<Issue> = getIssueDefaults()
|
||||||
|
let thisRef: HTMLDivElement
|
||||||
|
let focusIssueTitle: () => void
|
||||||
|
let labels: TagReference[] = []
|
||||||
|
|
||||||
|
let currentTeam: Ref<Team>
|
||||||
|
|
||||||
|
$: if (currentTeam === undefined) {
|
||||||
|
currentTeam = teams?.values().next()?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const key: KeyedAttribute = {
|
||||||
|
key: 'labels',
|
||||||
|
attr: client.getHierarchy().getAttribute(tracker.class.Issue, 'labels')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIssueDefaults (): AttachedData<Issue> {
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
assignee: null,
|
||||||
|
project: null,
|
||||||
|
number: 0,
|
||||||
|
rank: '',
|
||||||
|
status: '' as Ref<IssueStatus>,
|
||||||
|
priority: IssuePriority.NoPriority,
|
||||||
|
dueDate: null,
|
||||||
|
comments: 0,
|
||||||
|
subIssues: 0,
|
||||||
|
parents: [],
|
||||||
|
relations: [{ _id: related?._id, _class: related?._class }],
|
||||||
|
sprint: undefined,
|
||||||
|
estimation: 0,
|
||||||
|
reportedTime: 0,
|
||||||
|
reports: 0,
|
||||||
|
childInfo: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetToDefaults () {
|
||||||
|
newIssue = getIssueDefaults()
|
||||||
|
focusIssueTitle?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTitle (value: string) {
|
||||||
|
return value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close () {
|
||||||
|
dispatch('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createIssue () {
|
||||||
|
if (!canSave) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOne = await client.findOne<Issue>(
|
||||||
|
tracker.class.Issue,
|
||||||
|
{ space: currentTeam },
|
||||||
|
{ sort: { rank: SortingOrder.Descending } }
|
||||||
|
)
|
||||||
|
const incResult = await client.updateDoc(
|
||||||
|
tracker.class.Team,
|
||||||
|
core.space.Space,
|
||||||
|
currentTeam,
|
||||||
|
{ $inc: { sequence: 1 } },
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
const value: AttachedData<Issue> = {
|
||||||
|
...newIssue,
|
||||||
|
title: getTitle(newIssue.title),
|
||||||
|
number: (incResult as any).object.sequence,
|
||||||
|
rank: calcRank(lastOne, undefined),
|
||||||
|
parents: [{ parentId: tracker.ids.NoParent, parentTitle: '' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectId = await client.addCollection(
|
||||||
|
tracker.class.Issue,
|
||||||
|
currentTeam,
|
||||||
|
tracker.ids.NoParent,
|
||||||
|
tracker.class.Issue,
|
||||||
|
'subIssues',
|
||||||
|
value
|
||||||
|
)
|
||||||
|
for (const label of labels) {
|
||||||
|
await client.addCollection(label._class, label.space, objectId, tracker.class.Issue, 'labels', {
|
||||||
|
title: label.title,
|
||||||
|
color: label.color,
|
||||||
|
tag: label.tag
|
||||||
|
})
|
||||||
|
}
|
||||||
|
resetToDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTagRef (tag: TagElement): void {
|
||||||
|
labels = [
|
||||||
|
...labels,
|
||||||
|
{
|
||||||
|
_class: tags.class.TagReference,
|
||||||
|
_id: generateId() as Ref<TagReference>,
|
||||||
|
attachedTo: '' as Ref<Doc>,
|
||||||
|
attachedToClass: tracker.class.Issue,
|
||||||
|
collection: 'labels',
|
||||||
|
space: tags.space.Tags,
|
||||||
|
modifiedOn: 0,
|
||||||
|
modifiedBy: '' as Ref<Account>,
|
||||||
|
title: tag.title,
|
||||||
|
tag: tag._id,
|
||||||
|
color: tag.color
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
$: thisRef && thisRef.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
$: canSave = getTitle(newIssue.title ?? '').length > 0
|
||||||
|
$: if (!newIssue.status) {
|
||||||
|
const t = teams?.get(currentTeam)
|
||||||
|
if (t?.defaultIssueStatus !== undefined) {
|
||||||
|
newIssue.status = t?.defaultIssueStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={thisRef} class="flex-col root">
|
||||||
|
<div class="flex-row-top">
|
||||||
|
<div id="status-editor" class="mr-1">
|
||||||
|
<StatusEditor
|
||||||
|
value={newIssue}
|
||||||
|
statuses={issueStatuses.get(currentTeam)}
|
||||||
|
kind="transparent"
|
||||||
|
size="medium"
|
||||||
|
justify="center"
|
||||||
|
tooltipAlignment="bottom"
|
||||||
|
on:change={({ detail }) => (newIssue.status = detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex-col content">
|
||||||
|
<EditBox
|
||||||
|
bind:value={newIssue.title}
|
||||||
|
bind:focusInput={focusIssueTitle}
|
||||||
|
maxWidth="33rem"
|
||||||
|
placeholder={tracker.string.IssueTitlePlaceholder}
|
||||||
|
focus
|
||||||
|
/>
|
||||||
|
<div class="mt-4">
|
||||||
|
{#key newIssue.description}
|
||||||
|
<StyledTextArea
|
||||||
|
bind:content={newIssue.description}
|
||||||
|
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||||
|
showButtons={false}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex-between">
|
||||||
|
<div class="buttons-group xsmall-gap">
|
||||||
|
<SpaceSelector _class={tracker.class.Team} label={tracker.string.Team} bind:space={currentTeam} />
|
||||||
|
<PriorityEditor
|
||||||
|
value={newIssue}
|
||||||
|
shouldShowLabel
|
||||||
|
isEditable
|
||||||
|
kind="no-border"
|
||||||
|
size="small"
|
||||||
|
justify="center"
|
||||||
|
on:change={({ detail }) => (newIssue.priority = detail)}
|
||||||
|
/>
|
||||||
|
{#key newIssue.assignee}
|
||||||
|
<AssigneeEditor
|
||||||
|
value={newIssue}
|
||||||
|
size="small"
|
||||||
|
kind="no-border"
|
||||||
|
on:change={({ detail }) => (newIssue.assignee = detail)}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
<Component
|
||||||
|
is={tags.component.TagsDropdownEditor}
|
||||||
|
props={{
|
||||||
|
items: labels,
|
||||||
|
key,
|
||||||
|
targetClass: tracker.class.Issue,
|
||||||
|
countLabel: tracker.string.NumberLabels
|
||||||
|
}}
|
||||||
|
on:open={(evt) => {
|
||||||
|
addTagRef(evt.detail)
|
||||||
|
}}
|
||||||
|
on:delete={(evt) => {
|
||||||
|
labels = labels.filter((it) => it._id !== evt.detail)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<EstimationEditor kind={'no-border'} size={'small'} value={newIssue} />
|
||||||
|
</div>
|
||||||
|
<div class="buttons-group small-gap">
|
||||||
|
<Button label={presentation.string.Cancel} size="small" kind="transparent" on:click={close} />
|
||||||
|
<Button
|
||||||
|
disabled={!canSave}
|
||||||
|
label={presentation.string.Save}
|
||||||
|
size="small"
|
||||||
|
kind="no-border"
|
||||||
|
on:click={createIssue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.root {
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
background-color: var(--theme-bg-accent-color);
|
||||||
|
border: 1px solid var(--theme-button-border-enabled);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 0.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,120 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { Doc, DocumentQuery, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||||
|
import presentation, { createQuery, getClient } from '@anticrm/presentation'
|
||||||
|
import { calcRank, Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||||
|
import { Label, Spinner } from '@anticrm/ui'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import tracker from '../../../plugin'
|
||||||
|
import SubIssueList from '../edit/SubIssueList.svelte'
|
||||||
|
import CreateRelatedIssue from './CreateRelatedIssue.svelte'
|
||||||
|
|
||||||
|
export let object: Doc
|
||||||
|
export let isCreating = false
|
||||||
|
|
||||||
|
let query: DocumentQuery<Issue>
|
||||||
|
$: query = { 'relations._id': object._id, 'relations._class': object._class }
|
||||||
|
|
||||||
|
const subIssuesQuery = createQuery()
|
||||||
|
const client = getClient()
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let subIssues: Issue[] = []
|
||||||
|
|
||||||
|
let teams: Map<Ref<Team>, Team> | undefined
|
||||||
|
|
||||||
|
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
|
||||||
|
const { fromIndex, toIndex } = ev.detail
|
||||||
|
const [prev, next] = [
|
||||||
|
subIssues[fromIndex < toIndex ? toIndex : toIndex - 1],
|
||||||
|
subIssues[fromIndex < toIndex ? toIndex + 1 : toIndex]
|
||||||
|
]
|
||||||
|
const issue = subIssues[fromIndex]
|
||||||
|
|
||||||
|
await client.update(issue, { rank: calcRank(prev, next) })
|
||||||
|
}
|
||||||
|
|
||||||
|
$: subIssuesQuery.query(tracker.class.Issue, query, async (result) => (subIssues = result), {
|
||||||
|
sort: { rank: SortingOrder.Ascending }
|
||||||
|
})
|
||||||
|
|
||||||
|
const teamsQuery = createQuery()
|
||||||
|
|
||||||
|
$: teamsQuery.query(tracker.class.Team, {}, async (result) => {
|
||||||
|
teams = new Map(result.map((it) => [it._id, it]))
|
||||||
|
})
|
||||||
|
|
||||||
|
let issueStatuses = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
|
||||||
|
|
||||||
|
const statusesQuery = createQuery()
|
||||||
|
$: statusesQuery.query(
|
||||||
|
tracker.class.IssueStatus,
|
||||||
|
{},
|
||||||
|
(statuses) => {
|
||||||
|
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
|
||||||
|
for (const s of statuses) {
|
||||||
|
const id = s.attachedTo as Ref<Team>
|
||||||
|
st.set(id, [...(st.get(id) ?? []), s])
|
||||||
|
}
|
||||||
|
issueStatuses = st
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookup: { category: tracker.class.IssueStatusCategory },
|
||||||
|
sort: { rank: SortingOrder.Ascending }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
{#if subIssues !== undefined}
|
||||||
|
{#if issueStatuses.size > 0 && teams}
|
||||||
|
<SubIssueList issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
|
||||||
|
{:else}
|
||||||
|
<div class="p-1">
|
||||||
|
<Label label={presentation.string.NoMatchesFound} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isCreating}
|
||||||
|
<div class="pt-4">
|
||||||
|
<CreateRelatedIssue
|
||||||
|
related={object}
|
||||||
|
{issueStatuses}
|
||||||
|
{teams}
|
||||||
|
on:close={() => {
|
||||||
|
isCreating = false
|
||||||
|
dispatch('close')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="flex-center pt-3">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.list {
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
padding-top: 1px;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -75,6 +75,8 @@ import EstimationEditor from './components/issues/timereport/EstimationEditor.sv
|
|||||||
import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.svelte'
|
import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.svelte'
|
||||||
import TimeSpendReport from './components/issues/timereport/TimeSpendReport.svelte'
|
import TimeSpendReport from './components/issues/timereport/TimeSpendReport.svelte'
|
||||||
|
|
||||||
|
import RelatedIssues from './components/issues/related/RelatedIssues.svelte'
|
||||||
|
|
||||||
export async function queryIssue<D extends Issue> (
|
export async function queryIssue<D extends Issue> (
|
||||||
_class: Ref<Class<D>>,
|
_class: Ref<Class<D>>,
|
||||||
client: Client,
|
client: Client,
|
||||||
@ -189,7 +191,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
TimeSpendReport,
|
TimeSpendReport,
|
||||||
EstimationEditor,
|
EstimationEditor,
|
||||||
SubIssuesSelector,
|
SubIssuesSelector,
|
||||||
GrowPresenter
|
GrowPresenter,
|
||||||
|
RelatedIssues
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||||
|
@ -293,7 +293,8 @@ export default plugin(trackerId, {
|
|||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
Tracker: '' as AnyComponent,
|
Tracker: '' as AnyComponent,
|
||||||
TrackerApp: '' as AnyComponent
|
TrackerApp: '' as AnyComponent,
|
||||||
|
RelatedIssues: '' as AnyComponent
|
||||||
},
|
},
|
||||||
issueStatusCategory: {
|
issueStatusCategory: {
|
||||||
Backlog: '' as Ref<IssueStatusCategory>,
|
Backlog: '' as Ref<IssueStatusCategory>,
|
||||||
|
@ -17,11 +17,15 @@
|
|||||||
import { getPlatformColor } from '@anticrm/ui'
|
import { getPlatformColor } from '@anticrm/ui'
|
||||||
|
|
||||||
export let value: boolean
|
export let value: boolean
|
||||||
|
export let trueColor = 0
|
||||||
|
export let falseColor = 11
|
||||||
|
export let useInvert = false
|
||||||
|
|
||||||
$: color = value ? getPlatformColor(0) : getPlatformColor(11)
|
$: val = useInvert ? !value : value
|
||||||
|
$: color = val ? getPlatformColor(trueColor) : getPlatformColor(falseColor)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if value}
|
{#if val}
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
<div class="pinned-container" style="background-color: {color};" />
|
<div class="pinned-container" style="background-color: {color};" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,5 +30,7 @@
|
|||||||
<ClassAttributeBar _class={object._class} {object} {ignoreKeys} to={undefined} {allowedCollections} on:update />
|
<ClassAttributeBar _class={object._class} {object} {ignoreKeys} to={undefined} {allowedCollections} on:update />
|
||||||
{#each mixins as mixin}
|
{#each mixins as mixin}
|
||||||
{@const to = !hierarchy.hasMixin(mixin, setting.mixin.UserMixin) ? object._class : mixin.extends}
|
{@const to = !hierarchy.hasMixin(mixin, setting.mixin.UserMixin) ? object._class : mixin.extends}
|
||||||
<ClassAttributeBar _class={mixin._id} {object} {ignoreKeys} {to} {allowedCollections} on:update />
|
{#if !hierarchy.hasMixin(mixin, setting.mixin.Editable) || hierarchy.as(mixin, setting.mixin.Editable).value}
|
||||||
|
<ClassAttributeBar _class={mixin._id} {object} {ignoreKeys} {to} {allowedCollections} on:update />
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
import view from '@anticrm/view'
|
import view from '@anticrm/view'
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||||
import { ContextMenu } from '..'
|
import { ContextMenu } from '..'
|
||||||
import { fieldsFilter, getCollectionCounter, getFiltredKeys } from '../utils'
|
import { categorizeFields, getCollectionCounter, getFiltredKeys } from '../utils'
|
||||||
import ActionContext from './ActionContext.svelte'
|
import ActionContext from './ActionContext.svelte'
|
||||||
import DocAttributeBar from './DocAttributeBar.svelte'
|
import DocAttributeBar from './DocAttributeBar.svelte'
|
||||||
import UpDownNavigator from './UpDownNavigator.svelte'
|
import UpDownNavigator from './UpDownNavigator.svelte'
|
||||||
@ -112,12 +112,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const filtredKeys = Array.from(keysMap.values())
|
const filtredKeys = Array.from(keysMap.values())
|
||||||
keys = fieldsFilter(hierarchy, filtredKeys, false, allowedCollections).map((it) => it.key)
|
|
||||||
|
|
||||||
const fieldKeys = fieldsFilter(hierarchy, filtredKeys, true, collectionArrays)
|
const { attributes, collections } = categorizeFields(hierarchy, filtredKeys, collectionArrays, allowedCollections)
|
||||||
|
keys = attributes.map((it) => it.key)
|
||||||
|
|
||||||
const editors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = []
|
const editors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = []
|
||||||
const newInplaceAttributes = []
|
const newInplaceAttributes = []
|
||||||
for (const k of fieldKeys) {
|
for (const k of collections) {
|
||||||
if (allowedCollections.includes(k.key.key)) continue
|
if (allowedCollections.includes(k.key.key)) continue
|
||||||
const editor = await getFieldEditor(k.key)
|
const editor = await getFieldEditor(k.key)
|
||||||
if (editor === undefined) continue
|
if (editor === undefined) continue
|
||||||
|
@ -406,20 +406,44 @@ export function getFiltredKeys (
|
|||||||
return filterKeys(hierarchy, keys, ignoreKeys)
|
return filterKeys(hierarchy, keys, ignoreKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fieldsFilter (
|
export interface CategoryKey {
|
||||||
|
key: KeyedAttribute
|
||||||
|
category: AttributeCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
export function categorizeFields (
|
||||||
hierarchy: Hierarchy,
|
hierarchy: Hierarchy,
|
||||||
keys: KeyedAttribute[],
|
keys: KeyedAttribute[],
|
||||||
get: boolean,
|
useAsCollection: string[],
|
||||||
include: string[]
|
useAsAttribute: string[]
|
||||||
): Array<{ key: KeyedAttribute, category: AttributeCategory }> {
|
): {
|
||||||
const result: Array<{ key: KeyedAttribute, category: AttributeCategory }> = []
|
attributes: CategoryKey[]
|
||||||
|
collections: CategoryKey[]
|
||||||
|
} {
|
||||||
|
const result = {
|
||||||
|
attributes: [] as CategoryKey[],
|
||||||
|
collections: [] as CategoryKey[]
|
||||||
|
}
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const cl = getAttributePresenterClass(hierarchy, key.attr)
|
const cl = getAttributePresenterClass(hierarchy, key.attr)
|
||||||
if (include.includes(key.key)) {
|
if (useAsCollection.includes(key.key)) {
|
||||||
result.push({ key: key, category: cl.category })
|
result.collections.push({ key, category: cl.category })
|
||||||
} else if ((cl.category === 'collection') === get || (cl.category === 'inplace') === get) {
|
} else if (useAsAttribute.includes(key.key)) {
|
||||||
result.push({ key, category: cl.category })
|
result.attributes.push({ key, category: cl.category })
|
||||||
|
} else if (cl.category === 'collection' || cl.category === 'inplace') {
|
||||||
|
result.collections.push({ key, category: cl.category })
|
||||||
|
} else if (cl.category === 'array') {
|
||||||
|
const attrClass = getAttributePresenterClass(hierarchy, key.attr)
|
||||||
|
const clazz = hierarchy.getClass(attrClass.attrClass)
|
||||||
|
const mix = hierarchy.as(clazz, view.mixin.ArrayEditor)
|
||||||
|
if (mix.editor !== undefined && mix.inlineEditor === undefined) {
|
||||||
|
result.collections.push({ key, category: cl.category })
|
||||||
|
} else {
|
||||||
|
result.attributes.push({ key, category: cl.category })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.attributes.push({ key, category: cl.category })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"@anticrm/recruit": "~0.6.3",
|
"@anticrm/recruit": "~0.6.3",
|
||||||
"@anticrm/view": "~0.6.0",
|
"@anticrm/view": "~0.6.0",
|
||||||
"@anticrm/login": "~0.6.1",
|
"@anticrm/login": "~0.6.1",
|
||||||
"@anticrm/workbench": "~0.6.1"
|
"@anticrm/workbench": "~0.6.1",
|
||||||
|
"@anticrm/contact": "~0.6.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,14 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import recruit, { Applicant, recruitId, Vacancy } from '@anticrm/recruit'
|
import contact from '@anticrm/contact'
|
||||||
import { Doc } from '@anticrm/core'
|
import core, { Doc, Tx, TxCreateDoc, TxProcessor, TxRemoveDoc, TxUpdateDoc } from '@anticrm/core'
|
||||||
import login from '@anticrm/login'
|
import login from '@anticrm/login'
|
||||||
import { getMetadata } from '@anticrm/platform'
|
import { getMetadata } from '@anticrm/platform'
|
||||||
import { workbenchId } from '@anticrm/workbench'
|
import recruit, { Applicant, recruitId, Vacancy } from '@anticrm/recruit'
|
||||||
|
import { TriggerControl } from '@anticrm/server-core'
|
||||||
import view from '@anticrm/view'
|
import view from '@anticrm/view'
|
||||||
|
import { workbenchId } from '@anticrm/workbench'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -54,6 +56,106 @@ export function applicationTextPresenter (doc: Doc): string {
|
|||||||
return `APP-${applicant.number}`
|
return `APP-${applicant.number}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function OnVacancyUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||||
|
const actualTx = TxProcessor.extractTx(tx)
|
||||||
|
|
||||||
|
if (actualTx._class === core.class.TxCreateDoc) {
|
||||||
|
const createTx = actualTx as TxCreateDoc<Vacancy>
|
||||||
|
if (control.hierarchy.isDerived(createTx.objectClass, recruit.class.Vacancy)) {
|
||||||
|
const vacancy = TxProcessor.createDoc2Doc(createTx)
|
||||||
|
const res: Tx[] = []
|
||||||
|
if (vacancy.company !== undefined) {
|
||||||
|
return [
|
||||||
|
control.txFactory.createTxMixin(
|
||||||
|
vacancy.company,
|
||||||
|
contact.class.Organization,
|
||||||
|
contact.space.Contacts,
|
||||||
|
recruit.mixin.VacancyList,
|
||||||
|
{
|
||||||
|
$inc: { vacancies: 1 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualTx._class === core.class.TxUpdateDoc) {
|
||||||
|
const updateTx = actualTx as TxUpdateDoc<Vacancy>
|
||||||
|
if (control.hierarchy.isDerived(updateTx.objectClass, recruit.class.Vacancy)) {
|
||||||
|
if (updateTx.operations.company !== undefined) {
|
||||||
|
// It could be null or new value
|
||||||
|
const txes = await control.findAll(core.class.TxCUD, {
|
||||||
|
objectId: updateTx.objectId,
|
||||||
|
_id: { $nin: [updateTx._id] }
|
||||||
|
})
|
||||||
|
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
||||||
|
const res: Tx[] = []
|
||||||
|
if (vacancy.company != null) {
|
||||||
|
// We have old value
|
||||||
|
res.push(
|
||||||
|
control.txFactory.createTxMixin(
|
||||||
|
vacancy.company,
|
||||||
|
contact.class.Organization,
|
||||||
|
contact.space.Contacts,
|
||||||
|
recruit.mixin.VacancyList,
|
||||||
|
{
|
||||||
|
$inc: { vacancies: -1 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (updateTx.operations.company !== null) {
|
||||||
|
res.push(
|
||||||
|
control.txFactory.createTxMixin(
|
||||||
|
updateTx.operations.company,
|
||||||
|
contact.class.Organization,
|
||||||
|
contact.space.Contacts,
|
||||||
|
recruit.mixin.VacancyList,
|
||||||
|
{
|
||||||
|
$inc: { vacancies: 1 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actualTx._class === core.class.TxRemoveDoc) {
|
||||||
|
const removeTx = actualTx as TxRemoveDoc<Vacancy>
|
||||||
|
if (control.hierarchy.isDerived(removeTx.objectClass, recruit.class.Vacancy)) {
|
||||||
|
// It could be null or new value
|
||||||
|
const txes = await control.findAll(core.class.TxCUD, {
|
||||||
|
objectId: removeTx.objectId,
|
||||||
|
_id: { $nin: [removeTx._id] }
|
||||||
|
})
|
||||||
|
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
||||||
|
const res: Tx[] = []
|
||||||
|
if (vacancy.company != null) {
|
||||||
|
// We have old value
|
||||||
|
res.push(
|
||||||
|
control.txFactory.createTxMixin(
|
||||||
|
vacancy.company,
|
||||||
|
contact.class.Organization,
|
||||||
|
contact.space.Contacts,
|
||||||
|
recruit.mixin.VacancyList,
|
||||||
|
{
|
||||||
|
$inc: { vacancies: -1 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export default async () => ({
|
export default async () => ({
|
||||||
function: {
|
function: {
|
||||||
@ -61,5 +163,8 @@ export default async () => ({
|
|||||||
VacancyTextPresenter: vacancyTextPresenter,
|
VacancyTextPresenter: vacancyTextPresenter,
|
||||||
ApplicationHTMLPresenter: applicationHTMLPresenter,
|
ApplicationHTMLPresenter: applicationHTMLPresenter,
|
||||||
ApplicationTextPresenter: applicationTextPresenter
|
ApplicationTextPresenter: applicationTextPresenter
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
OnVacancyUpdate
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import type { Resource, Plugin } from '@anticrm/platform'
|
import type { Resource, Plugin } from '@anticrm/platform'
|
||||||
import { plugin } from '@anticrm/platform'
|
import { plugin } from '@anticrm/platform'
|
||||||
import { Doc } from '@anticrm/core'
|
import { Doc } from '@anticrm/core'
|
||||||
|
import { TriggerFunc } from '@anticrm/server-core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -31,5 +32,8 @@ export default plugin(serverRecruitId, {
|
|||||||
ApplicationTextPresenter: '' as Resource<(doc: Doc) => string>,
|
ApplicationTextPresenter: '' as Resource<(doc: Doc) => string>,
|
||||||
VacancyHTMLPresenter: '' as Resource<(doc: Doc) => string>,
|
VacancyHTMLPresenter: '' as Resource<(doc: Doc) => string>,
|
||||||
VacancyTextPresenter: '' as Resource<(doc: Doc) => string>
|
VacancyTextPresenter: '' as Resource<(doc: Doc) => string>
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
OnVacancyUpdate: '' as Resource<TriggerFunc>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user