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
|
||||
})
|
||||
|
||||
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, {
|
||||
presenter: contact.component.EmployeeAccountPresenter
|
||||
})
|
||||
@ -388,9 +392,13 @@ export function createModel (builder: Builder): void {
|
||||
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(
|
||||
presentation.class.ObjectSearchCategory,
|
||||
|
@ -41,7 +41,8 @@ export default mergeIds(contactId, contact, {
|
||||
EditMember: '' as AnyComponent,
|
||||
EmployeeArrayEditor: '' as AnyComponent,
|
||||
EmployeeEditor: '' as AnyComponent,
|
||||
CreateEmployee: '' as AnyComponent
|
||||
CreateEmployee: '' as AnyComponent,
|
||||
AccountArrayEditor: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Persons: '' as IntlString,
|
||||
|
@ -21,6 +21,7 @@ export default mergeIds(coreId, core, {
|
||||
Private: '' as IntlString,
|
||||
Archived: '' 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 { 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 { TDoc } from './core'
|
||||
|
||||
@ -39,6 +39,7 @@ export class TSpace extends TDoc implements Space {
|
||||
@Prop(TypeBoolean(), core.string.Archived)
|
||||
archived!: boolean
|
||||
|
||||
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
|
||||
members!: Arr<Ref<Account>>
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,9 @@ export function createModel (builder: Builder): void {
|
||||
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(
|
||||
view.class.Viewlet,
|
||||
|
@ -93,7 +93,9 @@ export function createModel (builder: Builder): void {
|
||||
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(
|
||||
workbench.class.Application,
|
||||
|
@ -16,6 +16,7 @@
|
||||
import type { Employee, Organization } from '@anticrm/contact'
|
||||
import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@anticrm/core'
|
||||
import {
|
||||
ArrOf,
|
||||
Builder,
|
||||
Collection,
|
||||
Index,
|
||||
@ -32,17 +33,17 @@ import {
|
||||
import attachment from '@anticrm/model-attachment'
|
||||
import calendar from '@anticrm/model-calendar'
|
||||
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 presentation from '@anticrm/model-presentation'
|
||||
import tags from '@anticrm/model-tags'
|
||||
import task, { TSpaceWithStates, TTask, actionTemplates } from '@anticrm/model-task'
|
||||
import view, { createAction, actionTemplates as viewTemplates } from '@anticrm/model-view'
|
||||
import task, { actionTemplates, TSpaceWithStates, TTask } from '@anticrm/model-task'
|
||||
import view, { actionTemplates as viewTemplates, createAction } from '@anticrm/model-view'
|
||||
import workbench, { Application, createNavigateAction } from '@anticrm/model-workbench'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { Applicant, Candidate, Candidates, recruitId, Vacancy } from '@anticrm/recruit'
|
||||
import { KeyBinding } from '@anticrm/view'
|
||||
import { Applicant, Candidate, Candidates, recruitId, Vacancy, VacancyList } from '@anticrm/recruit'
|
||||
import setting from '@anticrm/setting'
|
||||
import { KeyBinding } from '@anticrm/view'
|
||||
import recruit from './plugin'
|
||||
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
|
||||
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)
|
||||
comments?: number
|
||||
|
||||
@Prop(Collection(chunter.class.Backlink), chunter.string.Comments)
|
||||
relations!: number
|
||||
}
|
||||
|
||||
@Model(recruit.class.Candidates, core.class.Space)
|
||||
@ -102,6 +106,13 @@ export class TCandidate extends TPerson implements Candidate {
|
||||
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)
|
||||
@UX(recruit.string.Application, recruit.icon.Application, recruit.string.ApplicationShort, 'number')
|
||||
export class TApplicant extends TTask implements Applicant {
|
||||
@ -126,7 +137,7 @@ export class TApplicant extends TTask implements Applicant {
|
||||
}
|
||||
|
||||
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, {
|
||||
view: {
|
||||
@ -140,13 +151,24 @@ export function createModel (builder: Builder): void {
|
||||
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, {
|
||||
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 talentsId = 'talents'
|
||||
|
@ -13,11 +13,15 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Organization } from '@anticrm/contact'
|
||||
import core, { Doc, Ref, Space, TxOperations } from '@anticrm/core'
|
||||
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
|
||||
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 { createKanbanTemplate, createSequence } from '@anticrm/model-task'
|
||||
import { Vacancy } from '@anticrm/recruit'
|
||||
import { getCategories } from '@anticrm/skillset'
|
||||
import { KanbanTemplate } from '@anticrm/task'
|
||||
import recruit from './plugin'
|
||||
@ -34,6 +38,33 @@ export const recruitOperation: MigrateOperation = {
|
||||
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> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
|
@ -56,7 +56,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
GotoSkills: '' as IntlString,
|
||||
GotoAssigned: '' as IntlString,
|
||||
GotoApplicants: '' as IntlString,
|
||||
GotoRecruitApplication: '' as IntlString
|
||||
GotoRecruitApplication: '' as IntlString,
|
||||
VacancyList: '' as IntlString
|
||||
},
|
||||
validator: {
|
||||
ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
|
||||
@ -81,7 +82,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
Opinions: '' as AnyComponent,
|
||||
OpinionPresenter: '' as AnyComponent,
|
||||
NewCandidateHeader: '' as AnyComponent,
|
||||
ApplicantFilter: '' as AnyComponent
|
||||
ApplicantFilter: '' as AnyComponent,
|
||||
VacancyList: '' as AnyComponent
|
||||
},
|
||||
template: {
|
||||
DefaultVacancy: '' as Ref<KanbanTemplate>,
|
||||
|
@ -19,6 +19,7 @@ import core from '@anticrm/core'
|
||||
import recruit from '@anticrm/recruit'
|
||||
import view from '@anticrm/view'
|
||||
import serverRecruit from '@anticrm/server-recruit'
|
||||
import serverCore from '@anticrm/server-core'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
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, {
|
||||
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)
|
||||
export class TEditable extends TClass implements Editable {}
|
||||
export class TEditable extends TClass implements Editable {
|
||||
value!: boolean
|
||||
}
|
||||
|
||||
@Mixin(setting.mixin.UserMixin, core.class.Class)
|
||||
export class TUserMixin extends TClass implements UserMixin {}
|
||||
@ -332,6 +334,8 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
setting.action.DeleteMixin
|
||||
)
|
||||
|
||||
// builder.mixin(core.class.Space, core.class.Class, setting.mixin.Editable, {})
|
||||
}
|
||||
|
||||
export { settingOperation } from './migration'
|
||||
|
@ -567,7 +567,9 @@ export function createModel (builder: Builder): void {
|
||||
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, {
|
||||
inlineEditor: tracker.component.ProjectStatusEditor
|
||||
|
@ -24,6 +24,7 @@
|
||||
"Collection": "Collection",
|
||||
"Array": "Array",
|
||||
"Bag": "Bag",
|
||||
"Enum": "Enum"
|
||||
"Enum": "Enum",
|
||||
"Members": "Members"
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@
|
||||
"Collection": "Коллекция",
|
||||
"Array": "Массив",
|
||||
"Bag": "Bag",
|
||||
"Enum": "Справочник"
|
||||
"Enum": "Справочник",
|
||||
"Members": "Участники"
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { IntlString, Asset } from '@anticrm/platform'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -18,7 +18,7 @@
|
||||
"Spaces": "Пространства",
|
||||
"Unassigned": "Не назначен",
|
||||
"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 {В # местах}}",
|
||||
"InThis": "В этом {space}",
|
||||
"NoMatchesInThis": "В этом {space} совпадения не обнаружены",
|
||||
|
@ -38,6 +38,7 @@
|
||||
export let width: string | undefined = undefined
|
||||
export let focusIndex = -1
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
export let showNavigate = true
|
||||
</script>
|
||||
|
||||
<UserBox
|
||||
@ -56,5 +57,6 @@
|
||||
{width}
|
||||
{focusIndex}
|
||||
{showTooltip}
|
||||
{showNavigate}
|
||||
on:change
|
||||
/>
|
||||
|
@ -16,17 +16,23 @@
|
||||
<script lang="ts">
|
||||
import contact, { Contact, formatName } from '@anticrm/contact'
|
||||
import type { Class, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { Asset, getEmbeddedLabel, IntlString } from '@anticrm/platform'
|
||||
import {
|
||||
ActionIcon,
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
getFocusManager,
|
||||
Icon,
|
||||
IconOpen,
|
||||
Label,
|
||||
LabelAndProps,
|
||||
showPanel,
|
||||
showPopup,
|
||||
LabelAndProps
|
||||
tooltip
|
||||
} from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation from '..'
|
||||
import { ObjectCreate } from '../types'
|
||||
@ -52,6 +58,7 @@
|
||||
export let width: string | undefined = undefined
|
||||
export let focusIndex = -1
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
export let showNavigate = true
|
||||
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
|
||||
@ -110,17 +117,15 @@
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="min-w-0" class:w-full={width === '100%'}>
|
||||
<Button
|
||||
{focusIndex}
|
||||
icon={hideIcon || selected ? undefined : icon}
|
||||
width={width ?? 'min-content'}
|
||||
{size}
|
||||
{kind}
|
||||
{justify}
|
||||
{showTooltip}
|
||||
on:click={_click}
|
||||
<Button {focusIndex} width={width ?? 'min-content'} {size} {kind} {justify} {showTooltip} on:click={_click}>
|
||||
<span slot="content" class="overflow-label flex-grow" class:flex-between={showNavigate && selected}>
|
||||
<div
|
||||
class="disabled"
|
||||
style:width={showNavigate && selected
|
||||
? `calc(${width ?? 'min-content'} - 1.5rem)`
|
||||
: `${width ?? 'min-content'}`}
|
||||
use:tooltip={selected !== undefined ? { label: getEmbeddedLabel(getName(selected)) } : undefined}
|
||||
>
|
||||
<span slot="content" class="overflow-label disabled">
|
||||
{#if selected}
|
||||
{#if hideIcon || selected}
|
||||
<UserInfo value={selected} size={kind === 'link' ? 'x-small' : 'tiny'} {icon} />
|
||||
@ -128,7 +133,26 @@
|
||||
{getName(selected)}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex-row-center">
|
||||
{#if icon}
|
||||
<Icon {icon} size={kind === 'link' ? 'small' : size} />
|
||||
{/if}
|
||||
<div class="ml-2">
|
||||
<Label {label} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selected && showNavigate}
|
||||
<ActionIcon
|
||||
icon={IconOpen}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
if (selected) {
|
||||
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
|
@ -132,9 +132,9 @@
|
||||
}
|
||||
|
||||
&.dialog {
|
||||
width: 40rem;
|
||||
width: 45rem;
|
||||
height: max-content;
|
||||
max-width: 40rem;
|
||||
max-width: 60rem;
|
||||
max-height: calc(100vh - 2rem);
|
||||
|
||||
.antiCard-header {
|
||||
|
@ -32,7 +32,7 @@
|
||||
class="button {size}"
|
||||
use:tooltip={{ label, direction, props: labelProps }}
|
||||
tabindex="0"
|
||||
on:click|stopPropagation={action}
|
||||
on:click|stopPropagation|preventDefault={action}
|
||||
>
|
||||
<div class="icon {size}" class:invisible>
|
||||
<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 IconScale } from './components/icons/Scale.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 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}
|
||||
{justify}
|
||||
{width}
|
||||
allowDeselect
|
||||
titleDeselect={contact.string.Cancel}
|
||||
bind:value
|
||||
on:change={(e) => onChange(e.detail)}
|
||||
/>
|
||||
|
@ -16,12 +16,9 @@
|
||||
import { Organization } from '@anticrm/contact'
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { DropdownPopup, Label, showPopup, Button } from '@anticrm/ui'
|
||||
import { UserBox } from '@anticrm/presentation'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import { ListItem } from '@anticrm/ui/src/types'
|
||||
import contact from '../plugin'
|
||||
import Company from './icons/Company.svelte'
|
||||
|
||||
export let value: Ref<Organization> | undefined
|
||||
export let label: IntlString = contact.string.Organization
|
||||
@ -29,80 +26,21 @@
|
||||
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
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>
|
||||
|
||||
<div class="clear-mins" bind:this={tool} />
|
||||
<Button
|
||||
<UserBox
|
||||
_class={contact.class.Organization}
|
||||
{label}
|
||||
{value}
|
||||
{kind}
|
||||
{size}
|
||||
{justify}
|
||||
{width}
|
||||
{size}
|
||||
{kind}
|
||||
on:click={() => {
|
||||
if (!opened) {
|
||||
opened = true
|
||||
showPopup(DropdownPopup, { title: label, items, icon }, tool, (result) => {
|
||||
if (result) setValue(result)
|
||||
opened = false
|
||||
})
|
||||
}
|
||||
allowDeselect
|
||||
titleDeselect={contact.string.Cancel}
|
||||
on:change={(evt) => {
|
||||
onChange(evt.detail)
|
||||
}}
|
||||
>
|
||||
<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 EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svelte'
|
||||
import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte'
|
||||
import AccountArrayEditor from './components/AccountArrayEditor.svelte'
|
||||
import EmployeeBrowser from './components/EmployeeBrowser.svelte'
|
||||
import EmployeeEditor from './components/EmployeeEditor.svelte'
|
||||
import EmployeePresenter from './components/EmployeePresenter.svelte'
|
||||
@ -44,7 +45,6 @@ import MemberPresenter from './components/MemberPresenter.svelte'
|
||||
import Members from './components/Members.svelte'
|
||||
import OrganizationEditor from './components/OrganizationEditor.svelte'
|
||||
import OrganizationPresenter from './components/OrganizationPresenter.svelte'
|
||||
import OrganizationSelector from './components/OrganizationSelector.svelte'
|
||||
import PersonEditor from './components/PersonEditor.svelte'
|
||||
import PersonPresenter from './components/PersonPresenter.svelte'
|
||||
import SocialEditor from './components/SocialEditor.svelte'
|
||||
@ -55,7 +55,6 @@ export {
|
||||
ChannelsEditor,
|
||||
ContactPresenter,
|
||||
ChannelsView,
|
||||
OrganizationSelector,
|
||||
ChannelsDropdown,
|
||||
EmployeePresenter,
|
||||
PersonPresenter,
|
||||
@ -144,7 +143,8 @@ export default async (): Promise<Resources> => ({
|
||||
EditMember,
|
||||
EmployeeArrayEditor,
|
||||
EmployeeEditor,
|
||||
CreateEmployee
|
||||
CreateEmployee,
|
||||
AccountArrayEditor
|
||||
},
|
||||
completion: {
|
||||
EmployeeQuery: async (
|
||||
|
@ -92,7 +92,9 @@
|
||||
"CopyLink": "Copy link",
|
||||
"HasActiveApplicant":"Active Only",
|
||||
"HasNoActiveApplicant": "No Active",
|
||||
"NoneApplications": "None"
|
||||
"NoneApplications": "None",
|
||||
"RelatedIssues": "Related issues",
|
||||
"VacancyList": "Vacancies"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Please select talent",
|
||||
|
@ -94,7 +94,9 @@
|
||||
"CopyLink": "Копировать ссылку",
|
||||
"HasActiveApplicant":"Только активные",
|
||||
"HasNoActiveApplicant": "Не активные",
|
||||
"NoneApplications": "Отсутствуют"
|
||||
"NoneApplications": "Отсутствуют",
|
||||
"RelatedIssues": "Связанные задачи",
|
||||
"VacancyList": "Вакансии"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Пожалуйста выберите таланта",
|
||||
|
@ -56,6 +56,7 @@
|
||||
"@anticrm/rekoni": "~0.6.0",
|
||||
"@anticrm/notification": "~0.6.0",
|
||||
"@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 = ''
|
||||
const description: string = ''
|
||||
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 {
|
||||
return name === '' && templateId !== undefined
|
||||
@ -87,12 +88,15 @@
|
||||
_class={contact.class.Organization}
|
||||
label={recruit.string.Company}
|
||||
placeholder={recruit.string.Company}
|
||||
justify={'left'}
|
||||
bind:value={company}
|
||||
allowDeselect
|
||||
titleDeselect={recruit.string.UnAssignCompany}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
icon={Company}
|
||||
readonly={preserveCompany}
|
||||
showNavigate={false}
|
||||
create={{ component: contact.component.CreateOrganization, label: contact.string.CreateOrganization }}
|
||||
/>
|
||||
<Component
|
||||
|
@ -18,10 +18,11 @@
|
||||
import type { Ref } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import { Panel } from '@anticrm/panel'
|
||||
import { createQuery, getClient, MembersBox } from '@anticrm/presentation'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { Vacancy } from '@anticrm/recruit'
|
||||
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 { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
@ -58,6 +59,7 @@
|
||||
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
|
||||
}
|
||||
}
|
||||
let isCreateIssue = false
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
@ -79,8 +81,8 @@
|
||||
<ClassAttributeBar
|
||||
{object}
|
||||
_class={object._class}
|
||||
ignoreKeys={['name', 'description', 'fullDescription']}
|
||||
to={core.class.Space}
|
||||
ignoreKeys={['name', 'description', 'fullDescription', 'private', 'archived']}
|
||||
to={core.class.Doc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -130,7 +132,41 @@
|
||||
space={object.space}
|
||||
attachments={object.attachments ?? 0}
|
||||
/>
|
||||
<MembersBox label={recruit.string.Members} space={object} />
|
||||
</Grid>
|
||||
<!-- <MembersBox label={recruit.string.Members} space={object} /> -->
|
||||
|
||||
<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>
|
||||
{/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 type { Contact, EmployeeAccount, Organization, Person } 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 { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
|
||||
import { Card, getClient, UserBox, UserBoxList } from '@anticrm/presentation'
|
||||
@ -171,7 +170,13 @@
|
||||
create={{ component: recruit.component.CreateCandidate, label: recruit.string.CreateTalent }}
|
||||
/>
|
||||
{/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
|
||||
bind:value={startDate}
|
||||
labelNull={recruit.string.StartDate}
|
||||
|
@ -46,7 +46,10 @@
|
||||
let candidate: Contact | undefined = undefined
|
||||
|
||||
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)
|
||||
|
@ -51,6 +51,7 @@ import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svel
|
||||
import VacancyPresenter from './components/VacancyPresenter.svelte'
|
||||
import recruit from './plugin'
|
||||
import { copyToClipboard, getApplicationTitle } from './utils'
|
||||
import VacancyList from './components/VacancyList.svelte'
|
||||
|
||||
async function createOpinion (object: Doc): Promise<void> {
|
||||
showPopup(CreateOpinion, { space: object.space, review: object._id })
|
||||
@ -287,7 +288,9 @@ export default async (): Promise<Resources> => ({
|
||||
|
||||
NewCandidateHeader,
|
||||
|
||||
ApplicantFilter
|
||||
ApplicantFilter,
|
||||
|
||||
VacancyList
|
||||
},
|
||||
completion: {
|
||||
ApplicationQuery: async (
|
||||
|
@ -105,7 +105,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
FullDescription: '' as IntlString,
|
||||
HasActiveApplicant: '' as IntlString,
|
||||
HasNoActiveApplicant: '' as IntlString,
|
||||
NoneApplications: '' as IntlString
|
||||
NoneApplications: '' as IntlString,
|
||||
RelatedIssues: '' as IntlString
|
||||
},
|
||||
space: {
|
||||
CandidatesPublic: '' as Ref<Space>
|
||||
|
@ -32,6 +32,13 @@ export interface Vacancy extends SpaceWithStates {
|
||||
company?: Ref<Organization>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface VacancyList extends Organization {
|
||||
vacancies: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -105,7 +112,8 @@ const recruit = plugin(recruitId, {
|
||||
Opinion: '' as Ref<Class<Opinion>>
|
||||
},
|
||||
mixin: {
|
||||
Candidate: '' as Ref<Mixin<Candidate>>
|
||||
Candidate: '' as Ref<Mixin<Candidate>>,
|
||||
VacancyList: '' as Ref<Mixin<VacancyList>>
|
||||
},
|
||||
component: {
|
||||
EditVacancy: '' as AnyComponent
|
||||
|
@ -36,7 +36,8 @@
|
||||
cls.extends === _class &&
|
||||
!cls.hidden &&
|
||||
[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)
|
||||
}
|
||||
|
@ -37,7 +37,12 @@
|
||||
let classes: Ref<Class<Doc>>[] = []
|
||||
clQuery.query(core.class.Class, {}, (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)
|
||||
})
|
||||
</script>
|
||||
|
@ -34,7 +34,9 @@
|
||||
icon: value.icon
|
||||
}
|
||||
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, {})
|
||||
dispatch('close')
|
||||
}
|
||||
|
@ -47,7 +47,9 @@ export interface Integration extends Doc {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Editable extends Class<Doc> {}
|
||||
export interface Editable extends Class<Doc> {
|
||||
value: boolean // true is editable, false is not
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -336,6 +336,7 @@
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
<div class="flex flex-wrap" style:gap={'0.2vw'}>
|
||||
{#if issueStatuses}
|
||||
<div id="status-editor">
|
||||
<StatusEditor
|
||||
@ -384,10 +385,11 @@
|
||||
{#if object.dueDate !== null}
|
||||
<DatePresenter bind:value={object.dueDate} editable />
|
||||
{/if}
|
||||
<ActionIcon icon={IconMoreH} size={'medium'} action={showMoreActions} />
|
||||
{:else}
|
||||
<Spinner size="small" />
|
||||
{/if}
|
||||
</div>
|
||||
<ActionIcon icon={IconMoreH} size={'medium'} action={showMoreActions} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
|
@ -53,6 +53,7 @@
|
||||
{size}
|
||||
{kind}
|
||||
{width}
|
||||
showNavigate={false}
|
||||
justify={'left'}
|
||||
showTooltip={{ label: tracker.string.AssignTo, direction: tooltipAlignment }}
|
||||
on:change={({ detail }) => handleAssigneeChanged(detail)}
|
||||
|
@ -172,11 +172,13 @@
|
||||
focus
|
||||
/>
|
||||
<div class="mt-4">
|
||||
{#key newIssue.description}
|
||||
<StyledTextArea
|
||||
bind:content={newIssue.description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
showButtons={false}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -275,8 +275,14 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
{#key issue._id}
|
||||
<SubIssues {issue} {issueStatuses} {currentTeam} />
|
||||
{#key issue._id && currentTeam !== undefined}
|
||||
{#if currentTeam !== undefined && issueStatuses !== undefined}
|
||||
<SubIssues
|
||||
{issue}
|
||||
issueStatuses={new Map([[currentTeam._id, issueStatuses]])}
|
||||
teams={new Map([[currentTeam?._id, currentTeam]])}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, WithLookup } from '@anticrm/core'
|
||||
import { Doc, Ref, WithLookup } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { getEventPositionElement, showPanel, showPopup } from '@anticrm/ui'
|
||||
import {
|
||||
@ -35,8 +35,9 @@
|
||||
import EstimationEditor from '../timereport/EstimationEditor.svelte'
|
||||
|
||||
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()
|
||||
|
||||
@ -99,6 +100,7 @@
|
||||
/>
|
||||
|
||||
{#each issues as issue, index (issue._id)}
|
||||
{@const currentTeam = teams.get(issue.space)}
|
||||
<div
|
||||
class="flex-between row"
|
||||
class:is-dragging={index === draggingIndex}
|
||||
@ -133,12 +135,14 @@
|
||||
justify={'left'}
|
||||
on:update={(result) => checkWidth('issue', result)}
|
||||
>
|
||||
{#if currentTeam}
|
||||
{getIssueId(currentTeam, issue)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
</span>
|
||||
<StatusEditor
|
||||
value={issue}
|
||||
statuses={issueStatuses}
|
||||
statuses={issueStatuses.get(issue.space)}
|
||||
justify="center"
|
||||
kind={'list'}
|
||||
size={'small'}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { SortingOrder, WithLookup } from '@anticrm/core'
|
||||
import { Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { calcRank, Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Button, Spinner, ExpandCollapse, closeTooltip, IconAdd } from '@anticrm/ui'
|
||||
@ -24,8 +24,8 @@
|
||||
import SubIssueList from './SubIssueList.svelte'
|
||||
|
||||
export let issue: Issue
|
||||
export let currentTeam: Team | undefined
|
||||
export let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
|
||||
const subIssuesQuery = createQuery()
|
||||
const client = getClient()
|
||||
@ -86,20 +86,29 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
{#if subIssues && issueStatuses && currentTeam}
|
||||
{#if subIssues && issueStatuses}
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
{#if hasSubIssues}
|
||||
<div class="list" class:collapsed={isCollapsed}>
|
||||
<SubIssueList issues={subIssues} {issueStatuses} {currentTeam} on:move={handleIssueSwap} />
|
||||
<SubIssueList issues={subIssues} {issueStatuses} {teams} on:move={handleIssueSwap} />
|
||||
</div>
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
{#if isCreating}
|
||||
{@const team = teams.get(issue.space)}
|
||||
{@const statuses = issueStatuses.get(issue.space)}
|
||||
{#if team !== undefined && statuses !== undefined}
|
||||
<div class="pt-4">
|
||||
<CreateSubIssue parentIssue={issue} {issueStatuses} {currentTeam} on:close={() => (isCreating = false)} />
|
||||
<CreateSubIssue
|
||||
parentIssue={issue}
|
||||
issueStatuses={statuses}
|
||||
currentTeam={team}
|
||||
on:close={() => (isCreating = false)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
{:else}
|
||||
<div class="flex-center pt-3">
|
||||
|
@ -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 TimeSpendReport from './components/issues/timereport/TimeSpendReport.svelte'
|
||||
|
||||
import RelatedIssues from './components/issues/related/RelatedIssues.svelte'
|
||||
|
||||
export async function queryIssue<D extends Issue> (
|
||||
_class: Ref<Class<D>>,
|
||||
client: Client,
|
||||
@ -189,7 +191,8 @@ export default async (): Promise<Resources> => ({
|
||||
TimeSpendReport,
|
||||
EstimationEditor,
|
||||
SubIssuesSelector,
|
||||
GrowPresenter
|
||||
GrowPresenter,
|
||||
RelatedIssues
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||
|
@ -293,7 +293,8 @@ export default plugin(trackerId, {
|
||||
},
|
||||
component: {
|
||||
Tracker: '' as AnyComponent,
|
||||
TrackerApp: '' as AnyComponent
|
||||
TrackerApp: '' as AnyComponent,
|
||||
RelatedIssues: '' as AnyComponent
|
||||
},
|
||||
issueStatusCategory: {
|
||||
Backlog: '' as Ref<IssueStatusCategory>,
|
||||
|
@ -17,11 +17,15 @@
|
||||
import { getPlatformColor } from '@anticrm/ui'
|
||||
|
||||
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>
|
||||
|
||||
{#if value}
|
||||
{#if val}
|
||||
<div class="flex-center">
|
||||
<div class="pinned-container" style="background-color: {color};" />
|
||||
</div>
|
||||
|
@ -30,5 +30,7 @@
|
||||
<ClassAttributeBar _class={object._class} {object} {ignoreKeys} to={undefined} {allowedCollections} on:update />
|
||||
{#each mixins as mixin}
|
||||
{@const to = !hierarchy.hasMixin(mixin, setting.mixin.UserMixin) ? object._class : mixin.extends}
|
||||
{#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}
|
||||
|
@ -32,7 +32,7 @@
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { ContextMenu } from '..'
|
||||
import { fieldsFilter, getCollectionCounter, getFiltredKeys } from '../utils'
|
||||
import { categorizeFields, getCollectionCounter, getFiltredKeys } from '../utils'
|
||||
import ActionContext from './ActionContext.svelte'
|
||||
import DocAttributeBar from './DocAttributeBar.svelte'
|
||||
import UpDownNavigator from './UpDownNavigator.svelte'
|
||||
@ -112,12 +112,13 @@
|
||||
}
|
||||
}
|
||||
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 newInplaceAttributes = []
|
||||
for (const k of fieldKeys) {
|
||||
for (const k of collections) {
|
||||
if (allowedCollections.includes(k.key.key)) continue
|
||||
const editor = await getFieldEditor(k.key)
|
||||
if (editor === undefined) continue
|
||||
|
@ -406,20 +406,44 @@ export function getFiltredKeys (
|
||||
return filterKeys(hierarchy, keys, ignoreKeys)
|
||||
}
|
||||
|
||||
export function fieldsFilter (
|
||||
export interface CategoryKey {
|
||||
key: KeyedAttribute
|
||||
category: AttributeCategory
|
||||
}
|
||||
|
||||
export function categorizeFields (
|
||||
hierarchy: Hierarchy,
|
||||
keys: KeyedAttribute[],
|
||||
get: boolean,
|
||||
include: string[]
|
||||
): Array<{ key: KeyedAttribute, category: AttributeCategory }> {
|
||||
const result: Array<{ key: KeyedAttribute, category: AttributeCategory }> = []
|
||||
useAsCollection: string[],
|
||||
useAsAttribute: string[]
|
||||
): {
|
||||
attributes: CategoryKey[]
|
||||
collections: CategoryKey[]
|
||||
} {
|
||||
const result = {
|
||||
attributes: [] as CategoryKey[],
|
||||
collections: [] as CategoryKey[]
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const cl = getAttributePresenterClass(hierarchy, key.attr)
|
||||
if (include.includes(key.key)) {
|
||||
result.push({ key: key, category: cl.category })
|
||||
} else if ((cl.category === 'collection') === get || (cl.category === 'inplace') === get) {
|
||||
result.push({ key, category: cl.category })
|
||||
if (useAsCollection.includes(key.key)) {
|
||||
result.collections.push({ key, category: cl.category })
|
||||
} else if (useAsAttribute.includes(key.key)) {
|
||||
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
|
||||
|
@ -32,6 +32,7 @@
|
||||
"@anticrm/recruit": "~0.6.3",
|
||||
"@anticrm/view": "~0.6.0",
|
||||
"@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.
|
||||
//
|
||||
|
||||
import recruit, { Applicant, recruitId, Vacancy } from '@anticrm/recruit'
|
||||
import { Doc } from '@anticrm/core'
|
||||
import contact from '@anticrm/contact'
|
||||
import core, { Doc, Tx, TxCreateDoc, TxProcessor, TxRemoveDoc, TxUpdateDoc } from '@anticrm/core'
|
||||
import login from '@anticrm/login'
|
||||
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 { workbenchId } from '@anticrm/workbench'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -54,6 +56,106 @@ export function applicationTextPresenter (doc: Doc): string {
|
||||
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
|
||||
export default async () => ({
|
||||
function: {
|
||||
@ -61,5 +163,8 @@ export default async () => ({
|
||||
VacancyTextPresenter: vacancyTextPresenter,
|
||||
ApplicationHTMLPresenter: applicationHTMLPresenter,
|
||||
ApplicationTextPresenter: applicationTextPresenter
|
||||
},
|
||||
trigger: {
|
||||
OnVacancyUpdate
|
||||
}
|
||||
})
|
||||
|
@ -16,6 +16,7 @@
|
||||
import type { Resource, Plugin } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import { Doc } from '@anticrm/core'
|
||||
import { TriggerFunc } from '@anticrm/server-core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -31,5 +32,8 @@ export default plugin(serverRecruitId, {
|
||||
ApplicationTextPresenter: '' as Resource<(doc: Doc) => string>,
|
||||
VacancyHTMLPresenter: '' as Resource<(doc: Doc) => string>,
|
||||
VacancyTextPresenter: '' as Resource<(doc: Doc) => string>
|
||||
},
|
||||
trigger: {
|
||||
OnVacancyUpdate: '' as Resource<TriggerFunc>
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user