mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-30 20:25:38 +00:00
Vacancies redesign (#1088)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
c06370e44c
commit
25df15e34a
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -16,7 +16,8 @@
|
||||
"METRICS_CONSOLE": "true", // Show metrics in console evert 30 seconds.,
|
||||
"MINIO_ENDPOINT": "localhost",
|
||||
"MINIO_ACCESS_KEY":"minioadmin",
|
||||
"MINIO_SECRET_KEY":"minioadmin"
|
||||
"MINIO_SECRET_KEY":"minioadmin",
|
||||
"SERVER_SECRET": "secret"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"sourceMaps": true,
|
||||
|
@ -61,6 +61,9 @@ export class TVacancy extends TSpaceWithStates implements Vacancy {
|
||||
@Prop(TypeString(), recruit.string.Company, contact.icon.Company)
|
||||
@Index(IndexKind.FullText)
|
||||
company?: string
|
||||
|
||||
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
|
||||
comments?: number
|
||||
}
|
||||
|
||||
@Model(recruit.class.Candidates, core.class.Space)
|
||||
@ -135,15 +138,15 @@ export function createModel (builder: Builder): void {
|
||||
hidden: false,
|
||||
navigatorModel: {
|
||||
spaces: [
|
||||
{
|
||||
label: recruit.string.Vacancies,
|
||||
spaceClass: recruit.class.Vacancy,
|
||||
addSpaceLabel: recruit.string.CreateVacancy,
|
||||
createComponent: recruit.component.CreateVacancy,
|
||||
component: recruit.component.EditVacancy
|
||||
}
|
||||
],
|
||||
specials: [
|
||||
{
|
||||
id: 'vacancies',
|
||||
component: recruit.component.Vacancies,
|
||||
icon: recruit.icon.Vacancy,
|
||||
label: recruit.string.Vacancies,
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
id: 'candidates',
|
||||
component: recruit.component.Candidates,
|
||||
@ -157,7 +160,8 @@ export function createModel (builder: Builder): void {
|
||||
icon: view.icon.Archive,
|
||||
label: workbench.string.Archive,
|
||||
position: 'top',
|
||||
visibleIf: workbench.function.HasArchiveSpaces
|
||||
visibleIf: workbench.function.HasArchiveSpaces,
|
||||
spaceClass: recruit.class.Vacancy
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
@ -358,6 +362,28 @@ export function createModel (builder: Builder): void {
|
||||
archived: true
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Action,
|
||||
core.space.Model,
|
||||
{
|
||||
label: recruit.string.EditVacancy,
|
||||
icon: recruit.icon.Vacancy,
|
||||
action: recruit.actionImpl.EditVacancy
|
||||
},
|
||||
recruit.action.EditVacancy
|
||||
)
|
||||
|
||||
builder.createDoc(view.class.ActionTarget, core.space.Model, {
|
||||
target: recruit.class.Vacancy,
|
||||
action: recruit.action.EditVacancy,
|
||||
query: {
|
||||
}
|
||||
})
|
||||
|
||||
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [view.action.Delete]
|
||||
})
|
||||
}
|
||||
|
||||
export { createDeps } from './creation'
|
||||
|
@ -25,16 +25,17 @@ import { ObjectSearchFactory, ObjectSearchCategory } from '@anticrm/model-presen
|
||||
|
||||
export default mergeIds(recruitId, recruit, {
|
||||
action: {
|
||||
CreateApplication: '' as Ref<Action>
|
||||
CreateApplication: '' as Ref<Action>,
|
||||
EditVacancy: '' as Ref<Action>
|
||||
},
|
||||
actionImpl: {
|
||||
CreateApplication: '' as Resource<(object: Doc) => Promise<void>>
|
||||
CreateApplication: '' as Resource<(object: Doc) => Promise<void>>,
|
||||
EditVacancy: '' as Resource<(object: Doc) => Promise<void>>
|
||||
},
|
||||
string: {
|
||||
ApplicationShort: '' as IntlString,
|
||||
ApplicationsShort: '' as IntlString,
|
||||
RecruitApplication: '' as IntlString,
|
||||
Vacancies: '' as IntlString,
|
||||
CandidatePools: '' as IntlString,
|
||||
SearchApplication: '' as IntlString,
|
||||
Application: '' as IntlString,
|
||||
@ -42,7 +43,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
FullDescription: '' as IntlString,
|
||||
Due: '' as IntlString,
|
||||
Source: '' as IntlString,
|
||||
ManageVacancyStatuses: '' as IntlString
|
||||
ManageVacancyStatuses: '' as IntlString,
|
||||
EditVacancy: '' as IntlString
|
||||
},
|
||||
validator: {
|
||||
ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
|
||||
@ -59,7 +61,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
Applications: '' as AnyComponent,
|
||||
Candidates: '' as AnyComponent,
|
||||
CreateCandidate: '' as AnyComponent,
|
||||
SkillsView: '' as AnyComponent
|
||||
SkillsView: '' as AnyComponent,
|
||||
Vacancies: '' as AnyComponent
|
||||
},
|
||||
template: {
|
||||
DefaultVacancy: '' as Ref<KanbanTemplate>
|
||||
|
@ -104,8 +104,13 @@ export abstract class MemDb extends TxProcessor {
|
||||
private async getReverseLookupValue<T extends Doc> (doc: T, lookup: ReverseLookups, result: LookupData<T>): Promise<void> {
|
||||
for (const key in lookup._id) {
|
||||
const value = lookup._id[key]
|
||||
const objects = await this.findAll(value, { attachedTo: doc._id })
|
||||
;(result as any)[key] = objects
|
||||
if (Array.isArray(value)) {
|
||||
const objects = await this.findAll(value[0], { [value[1]]: doc._id })
|
||||
;(result as any)[key] = objects
|
||||
} else {
|
||||
const objects = await this.findAll(value, { attachedTo: doc._id })
|
||||
;(result as any)[key] = objects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ export interface ReverseLookups {
|
||||
* @public
|
||||
*/
|
||||
export interface ReverseLookup {
|
||||
[key: string]: Ref<Class<AttachedDoc>>
|
||||
[key: string]: Ref<Class<AttachedDoc>> | [Ref<Class<Doc>>, string]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,6 +98,9 @@ export type FindOptions<T extends Doc> = {
|
||||
limit?: number
|
||||
sort?: SortingQuery<T>
|
||||
lookup?: Lookup<T>
|
||||
projection?: {
|
||||
[P in keyof T]?: 0 | 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -379,7 +379,17 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
): Promise<void> {
|
||||
for (const key in lookup._id) {
|
||||
const value = lookup._id[key]
|
||||
const objects = await this.findAll(value, { attachedTo: doc._id })
|
||||
|
||||
let _class: Ref<Class<Doc>>
|
||||
let attr = 'attachedTo'
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
_class = value[0]
|
||||
attr = value[1]
|
||||
} else {
|
||||
_class = value
|
||||
}
|
||||
const objects = await this.findAll(_class, { [attr]: doc._id })
|
||||
;(result as any)[key] = objects
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
import Spinner from './Spinner.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import Icon from './Icon.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let label: IntlString
|
||||
export let primary: boolean = false
|
||||
|
@ -49,7 +49,8 @@
|
||||
"PersonFirstNamePlaceholder": "John",
|
||||
"PersonLastNamePlaceholder": "Appleseed",
|
||||
"PersonLocationPlaceholder": "Location",
|
||||
"ManageVacancyStatuses": "Manage vacancy statuses"
|
||||
"ManageVacancyStatuses": "Manage vacancy statuses",
|
||||
"EditVacancy": "Edit"
|
||||
},
|
||||
"status": {
|
||||
"CandidateRequired": "Please select candidate",
|
||||
|
@ -49,7 +49,8 @@
|
||||
"PersonFirstNamePlaceholder": "John",
|
||||
"PersonLastNamePlaceholder": "Appleseed",
|
||||
"PersonLocationPlaceholder": "Местоположение",
|
||||
"ManageVacancyStatuses": "Управление статусами вакансии"
|
||||
"ManageVacancyStatuses": "Управление статусами вакансии",
|
||||
"EditVacancy": "Редактировать"
|
||||
},
|
||||
"status": {
|
||||
"CandidateRequired": "Пожалуйста выберите кандидата",
|
||||
|
@ -38,13 +38,13 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex-col h-full card-container" on:click={() => {
|
||||
showPanel(view.component.EditDoc, candidate._id, candidate._class, 'full')
|
||||
}}>
|
||||
<div class="flex-col h-full card-container">
|
||||
<div class="label">CANDIDATE</div>
|
||||
<Avatar avatar={candidate.avatar} size={'large'} />
|
||||
{#if candidate}
|
||||
<div class="name lines-limit-2">{formatName(candidate.name)}</div>
|
||||
<div class="name lines-limit-2 over-underline" on:click={() => {
|
||||
showPanel(view.component.EditDoc, candidate._id, candidate._class, 'full')
|
||||
}}>{formatName(candidate.name)}</div>
|
||||
<div class="description lines-limit-2">{candidate.title ?? ''}</div>
|
||||
<div class="description overflow-label">{candidate.city ?? ''}</div>
|
||||
<div class="footer flex flex-reverse flex-grow">
|
||||
@ -61,19 +61,11 @@
|
||||
|
||||
<style lang="scss">
|
||||
.card-container {
|
||||
cursor: pointer;
|
||||
padding: 1rem 1.5rem 1.25rem;
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
|
||||
&:hover {
|
||||
&:hover .name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.label {
|
||||
margin-bottom: 1.75rem;
|
||||
font-weight: 500;
|
||||
|
123
plugins/recruit-resources/src/components/Vacancies.svelte
Normal file
123
plugins/recruit-resources/src/components/Vacancies.svelte
Normal file
@ -0,0 +1,123 @@
|
||||
<!--
|
||||
// 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 } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Applicant, Vacancy } from '@anticrm/recruit'
|
||||
import { Button, getCurrentLocation, Icon, Label, navigate, Scroller, showPopup } from '@anticrm/ui'
|
||||
import SearchEdit from '@anticrm/ui/src/components/SearchEdit.svelte'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import recruit from '../plugin'
|
||||
import CreateVacancy from './CreateVacancy.svelte'
|
||||
|
||||
function action (vacancy: Ref<Vacancy>): void {
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = vacancy
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
let search: string = ''
|
||||
let resultQuery: DocumentQuery<Doc> = {}
|
||||
let vacancyQuery: DocumentQuery<Doc> = {}
|
||||
|
||||
async function updateResultQuery (search: string): Promise<void> {
|
||||
resultQuery = search === '' ? {} : { $search: search }
|
||||
}
|
||||
|
||||
let vacancies: Vacancy[] = []
|
||||
const query = createQuery()
|
||||
|
||||
$: query.query(recruit.class.Vacancy, { archived: false }, (res) => {
|
||||
vacancies = res
|
||||
})
|
||||
|
||||
$: if (vacancies.length > 0) {
|
||||
vacancyQuery = {
|
||||
_id: {
|
||||
$in: vacancies
|
||||
.filter((it) => it.name.includes(search) || it.description.includes(search) || it.company?.includes(search) || ((applications?.get(it._id) ?? 0) > 0))
|
||||
.map((it) => it._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let applications: Map<Ref<Vacancy>, number> | undefined
|
||||
|
||||
const applicantQuery = createQuery()
|
||||
$: if (vacancies.length > 0) {
|
||||
applicantQuery.query(
|
||||
recruit.class.Applicant,
|
||||
{ ...(resultQuery as DocumentQuery<Applicant>), space: { $in: vacancies.map((it) => it._id) } },
|
||||
(res) => {
|
||||
const result = new Map<Ref<Vacancy>, number>()
|
||||
|
||||
for (const d of res) {
|
||||
result.set(d.space, (result.get(d.space) ?? 0) + 1)
|
||||
}
|
||||
|
||||
applications = result
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(CreateVacancy, { space: recruit.space.CandidatesPublic }, ev.target as HTMLElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ac-header full">
|
||||
<div class="ac-header__wrap-title">
|
||||
<div class="ac-header__icon"><Icon icon={recruit.icon.Vacancy} size={'small'} /></div>
|
||||
<span class="ac-header__title"><Label label={recruit.string.Vacancies} /></span>
|
||||
</div>
|
||||
<SearchEdit
|
||||
bind:value={search}
|
||||
on:change={() => {
|
||||
updateResultQuery(search)
|
||||
}}
|
||||
/>
|
||||
<Button label={recruit.string.Create} primary={true} size={'small'} on:click={(ev) => showCreateDialog(ev)} />
|
||||
</div>
|
||||
<Scroller>
|
||||
<Table
|
||||
_class={recruit.class.Vacancy}
|
||||
config={[
|
||||
{
|
||||
key: '',
|
||||
presenter: recruit.component.VacancyItemPresenter,
|
||||
label: recruit.string.Vacancy,
|
||||
sortingKey: 'name',
|
||||
props: { action }
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
presenter: recruit.component.VacancyCountPresenter,
|
||||
label: recruit.string.Applications,
|
||||
props: { applications, resultQuery }
|
||||
},
|
||||
'company',
|
||||
'location',
|
||||
'description',
|
||||
'modifiedOn'
|
||||
]}
|
||||
options={{}}
|
||||
query={{
|
||||
...vacancyQuery,
|
||||
archived: false
|
||||
}}
|
||||
showNotification
|
||||
/>
|
||||
</Scroller>
|
@ -0,0 +1,54 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import core, { Doc, DocumentQuery, Ref, Space } from '@anticrm/core'
|
||||
import recruit from '@anticrm/recruit'
|
||||
import task from '@anticrm/task'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
|
||||
export let value: Ref<Space>
|
||||
export let resultQuery: DocumentQuery<Doc>
|
||||
|
||||
</script>
|
||||
|
||||
<div class='popup-table'>
|
||||
<Table
|
||||
_class={recruit.class.Applicant}
|
||||
config={['', '$lookup.attachedTo', '$lookup.state', '$lookup.doneState', 'modifiedOn']}
|
||||
options={
|
||||
{
|
||||
lookup: {
|
||||
state: task.class.State,
|
||||
space: core.class.Space,
|
||||
doneState: task.class.DoneState,
|
||||
attachedTo: recruit.mixin.Candidate
|
||||
},
|
||||
limit: 10
|
||||
}
|
||||
}
|
||||
query={ { ...(resultQuery ?? {}), space: value } }
|
||||
loadingProps={{ length: 0 }}
|
||||
/>
|
||||
</div>
|
||||
<style lang="scss">
|
||||
.popup-table {
|
||||
overflow: auto;
|
||||
// width: 70rem;
|
||||
// max-width: 70rem !important;
|
||||
max-height: 30rem;
|
||||
}
|
||||
</style>
|
@ -14,8 +14,9 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import Company from './icons/Company.svelte'
|
||||
import type { Vacancy } from '@anticrm/recruit'
|
||||
import { closePanel, closePopup, closeTooltip, getCurrentLocation, navigate } from '@anticrm/ui'
|
||||
import Company from './icons/Company.svelte'
|
||||
|
||||
export let vacancy: Vacancy
|
||||
</script>
|
||||
@ -26,7 +27,15 @@
|
||||
<Company size={'large'} />
|
||||
</div>
|
||||
{#if vacancy}
|
||||
<div class="name lines-limit-2">{vacancy.name}</div>
|
||||
<div class="name lines-limit-2 over-underline" on:click={() => {
|
||||
closeTooltip()
|
||||
closePopup()
|
||||
closePanel()
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = vacancy._id
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}}>{vacancy.name}</div>
|
||||
<div class="description lines-limit-2">{vacancy.description ?? ''}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -0,0 +1,34 @@
|
||||
<!--
|
||||
// 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 } from '@anticrm/core'
|
||||
import { Vacancy } from '@anticrm/recruit'
|
||||
import { Icon, Tooltip } from '@anticrm/ui'
|
||||
import recruit from '../plugin'
|
||||
import VacancyApplicationsPopup from './VacancyApplicationsPopup.svelte'
|
||||
|
||||
export let value: Vacancy
|
||||
export let applications: Map<Ref<Vacancy>, number> | undefined
|
||||
export let resultQuery: DocumentQuery<Doc>
|
||||
</script>
|
||||
|
||||
{#if (applications?.get(value._id) ?? 0) > 0}
|
||||
<Tooltip label={recruit.string.Applications} component={VacancyApplicationsPopup} props={{ value: value._id, resultQuery }}>
|
||||
<div class="sm-tool-icon">
|
||||
<span class="icon"><Icon icon={recruit.icon.Application} size={'small'} /></span> {(applications?.get(value._id) ?? 0)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
@ -0,0 +1,46 @@
|
||||
<!--
|
||||
// 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 { Ref } from '@anticrm/core'
|
||||
import type { Vacancy } from '@anticrm/recruit'
|
||||
import { ActionIcon, Icon, IconEdit, showPanel } from '@anticrm/ui'
|
||||
import recruit from '../plugin'
|
||||
|
||||
export let value: Vacancy
|
||||
export let action: (item: Ref<Vacancy>) => void
|
||||
|
||||
function editVacancy ():void {
|
||||
showPanel(recruit.component.EditVacancy, value._id, value._class, 'right')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="sm-tool-icon vacancy-item over-underline" on:click={() => (action(value._id))}>
|
||||
<span class="icon"><Icon icon={recruit.icon.Vacancy} size={'small'} /></span> {value.name}
|
||||
<div class='action ml-4'>
|
||||
<ActionIcon label={recruit.string.Edit} size={'small'} icon={IconEdit} action={editVacancy}/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<style type='scss'>
|
||||
.vacancy-item {
|
||||
&:hover .action {
|
||||
visibility: visible;
|
||||
}
|
||||
.action {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -27,7 +27,7 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="sm-tool-icon" on:click={show}>
|
||||
<div class="sm-tool-icon over-underline" on:click={show}>
|
||||
<span class="icon"><Icon icon={recruit.icon.Vacancy} size={'small'} /></span> {value.name}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -18,7 +18,7 @@ import { IntlString, OK, Resources, Severity, Status, translate } from '@anticrm
|
||||
import { ObjectSearchResult } from '@anticrm/presentation'
|
||||
import { Applicant } from '@anticrm/recruit'
|
||||
import task from '@anticrm/task'
|
||||
import { showPopup } from '@anticrm/ui'
|
||||
import { showPanel, showPopup } from '@anticrm/ui'
|
||||
import ApplicationItem from './components/ApplicationItem.svelte'
|
||||
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
|
||||
import Applications from './components/Applications.svelte'
|
||||
@ -32,13 +32,20 @@ import EditVacancy from './components/EditVacancy.svelte'
|
||||
import KanbanCard from './components/KanbanCard.svelte'
|
||||
import SkillsView from './components/SkillsView.svelte'
|
||||
import TemplatesIcon from './components/TemplatesIcon.svelte'
|
||||
import Vacancies from './components/Vacancies.svelte'
|
||||
import VacancyItemPresenter from './components/VacancyItemPresenter.svelte'
|
||||
import VacancyPresenter from './components/VacancyPresenter.svelte'
|
||||
import VacancyCountPresenter from './components/VacancyCountPresenter.svelte'
|
||||
import recruit from './plugin'
|
||||
|
||||
async function createApplication (object: Doc): Promise<void> {
|
||||
showPopup(CreateApplication, { candidate: object._id, preserveCandidate: true })
|
||||
}
|
||||
|
||||
async function editVacancy (object: Doc): Promise<void> {
|
||||
showPanel(recruit.component.EditVacancy, object._id, object._class, 'right')
|
||||
}
|
||||
|
||||
export async function applicantValidator (applicant: Applicant, client: Client): Promise<Status> {
|
||||
if (applicant.attachedTo === undefined) {
|
||||
return new Status(Severity.INFO, recruit.status.CandidateRequired, {})
|
||||
@ -92,7 +99,8 @@ export async function queryApplication (client: Client, search: string): Promise
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
actionImpl: {
|
||||
CreateApplication: createApplication
|
||||
CreateApplication: createApplication,
|
||||
EditVacancy: editVacancy
|
||||
},
|
||||
validator: {
|
||||
ApplicantValidator: applicantValidator
|
||||
@ -110,7 +118,10 @@ export default async (): Promise<Resources> => ({
|
||||
Candidates,
|
||||
CreateCandidate,
|
||||
VacancyPresenter,
|
||||
SkillsView
|
||||
SkillsView,
|
||||
Vacancies,
|
||||
VacancyItemPresenter,
|
||||
VacancyCountPresenter
|
||||
},
|
||||
completion: {
|
||||
ApplicationQuery: async (client: Client, query: string) => await queryApplication(client, query)
|
||||
|
@ -18,6 +18,7 @@ import type { IntlString, StatusCode } from '@anticrm/platform'
|
||||
import { mergeIds } from '@anticrm/platform'
|
||||
import recruit, { recruitId } from '@anticrm/recruit'
|
||||
import { TagCategory } from '@anticrm/tags'
|
||||
import { AnyComponent } from '@anticrm/ui'
|
||||
|
||||
export default mergeIds(recruitId, recruit, {
|
||||
status: {
|
||||
@ -66,7 +67,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
PersonFirstNamePlaceholder: '' as IntlString,
|
||||
PersonLastNamePlaceholder: '' as IntlString,
|
||||
Location: '' as IntlString,
|
||||
Title: '' as IntlString
|
||||
Title: '' as IntlString,
|
||||
Vacancies: '' as IntlString
|
||||
},
|
||||
space: {
|
||||
CandidatesPublic: '' as Ref<Space>
|
||||
@ -74,5 +76,9 @@ export default mergeIds(recruitId, recruit, {
|
||||
category: {
|
||||
Other: '' as Ref<TagCategory>,
|
||||
Category: '' as Ref<TagCategory>
|
||||
},
|
||||
component: {
|
||||
VacancyItemPresenter: '' as AnyComponent,
|
||||
VacancyCountPresenter: '' as AnyComponent
|
||||
}
|
||||
})
|
||||
|
@ -12,31 +12,32 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import core from '@anticrm/core'
|
||||
import type { Space } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { Label, Icon } from '@anticrm/ui'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Icon, Label } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { NavigatorModel } from '@anticrm/workbench'
|
||||
|
||||
import workbench from '../plugin'
|
||||
import { getSpecialSpaceClass } from '../utils'
|
||||
|
||||
export let model: NavigatorModel | undefined
|
||||
|
||||
|
||||
const query = createQuery()
|
||||
let spaceSample: Space | undefined
|
||||
$: if (model) {
|
||||
query.query(
|
||||
core.class.Space,
|
||||
{
|
||||
_class: { $in: model.spaces.map(x => x.spaceClass) },
|
||||
_class: { $in: getSpecialSpaceClass(model) },
|
||||
archived: true
|
||||
},
|
||||
(result) => { spaceSample = result[0] },
|
||||
(result) => {
|
||||
spaceSample = result[0]
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
@ -44,12 +45,14 @@
|
||||
let spaceName = ''
|
||||
$: {
|
||||
const spaceClass = spaceSample?._class ?? ''
|
||||
const spaceModel = model?.spaces.find(x => x.spaceClass == spaceClass)
|
||||
const spaceModel = model?.spaces.find((x) => x.spaceClass === spaceClass)
|
||||
|
||||
const label = spaceModel?.label
|
||||
|
||||
if (label) {
|
||||
void translate(label, {}).then((l) => { spaceName = l.toLowerCase() })
|
||||
translate(label, {}).then((l) => {
|
||||
spaceName = l.toLowerCase()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -59,17 +62,18 @@
|
||||
<div class="content-color mr-3"><Icon icon={view.icon.Archive} size={'medium'} /></div>
|
||||
<div class="fs-title"><Label label={workbench.string.Archived} params={{ object: spaceName }} /></div>
|
||||
</div>
|
||||
{#if spaceSample !== undefined}
|
||||
<Table
|
||||
_class={spaceSample._class}
|
||||
config={['name', 'company', 'location', 'modifiedOn']}
|
||||
options={{}}
|
||||
showNotification
|
||||
baseMenuClass={core.class.Space}
|
||||
query={{
|
||||
_class: { $in: model?.spaces.map(x => x.spaceClass) ?? [] },
|
||||
archived: true
|
||||
}} />
|
||||
{#if spaceSample !== undefined && model}
|
||||
<Table
|
||||
_class={spaceSample._class}
|
||||
config={['', 'company', 'location', 'modifiedOn']}
|
||||
options={{}}
|
||||
showNotification
|
||||
baseMenuClass={core.class.Space}
|
||||
query={{
|
||||
_class: { $in: getSpecialSpaceClass(model) },
|
||||
archived: true
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
import { Scroller } from '@anticrm/ui'
|
||||
import type { NavigatorModel, SpecialNavModel } from '@anticrm/workbench'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getSpecialSpaceClass } from '../utils'
|
||||
import SpacesNav from './navigator/SpacesNav.svelte'
|
||||
import SpecialElement from './navigator/SpecialElement.svelte'
|
||||
import TreeSeparator from './navigator/TreeSeparator.svelte'
|
||||
@ -31,11 +32,12 @@
|
||||
const query = createQuery()
|
||||
let spaces: Space[] = []
|
||||
let shownSpaces: Space[] = []
|
||||
|
||||
$: if (model) {
|
||||
query.query(
|
||||
core.class.Space,
|
||||
{
|
||||
_class: { $in: model.spaces.map(x => x.spaceClass) }
|
||||
_class: { $in: getSpecialSpaceClass(model) }
|
||||
},
|
||||
(result) => { spaces = result },
|
||||
{ sort: { name: SortingOrder.Ascending } })
|
||||
|
@ -15,11 +15,11 @@
|
||||
<script lang="ts">
|
||||
import contact, { Employee, EmployeeAccount } from '@anticrm/contact'
|
||||
import core, { Client, getCurrentAccount, Ref, Space } from '@anticrm/core'
|
||||
import notification, { NotificationStatus } from '@anticrm/notification'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { Avatar, createQuery, setClient } from '@anticrm/presentation'
|
||||
import {
|
||||
AnyComponent,
|
||||
closePanel,
|
||||
closePopup,
|
||||
AnyComponent, closePopup,
|
||||
closeTooltip,
|
||||
Component,
|
||||
getCurrentLocation,
|
||||
@ -41,8 +41,6 @@
|
||||
import NavHeader from './NavHeader.svelte'
|
||||
import Navigator from './Navigator.svelte'
|
||||
import SpaceView from './SpaceView.svelte'
|
||||
import notification, { NotificationStatus } from '@anticrm/notification'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
|
||||
export let client: Client
|
||||
|
||||
@ -74,6 +72,7 @@
|
||||
specialComponent = getSpecialComponent(currentFolder)
|
||||
if (specialComponent !== undefined) {
|
||||
currentSpecial = currentFolder
|
||||
currentSpace = undefined
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,15 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Class, Client, Obj, Ref } from '@anticrm/core'
|
||||
import type { Class, Client, Obj, Ref, Space } from '@anticrm/core'
|
||||
import type { Asset } from '@anticrm/platform'
|
||||
import { NavigatorModel } from '@anticrm/workbench'
|
||||
|
||||
export function classIcon (client: Client, _class: Ref<Class<Obj>>): Asset | undefined {
|
||||
return client.getHierarchy().getClass(_class).icon
|
||||
}
|
||||
export function getSpecialSpaceClass (model: NavigatorModel): Array<Ref<Class<Space>>> {
|
||||
const spaceResult = model.spaces.map(x => x.spaceClass)
|
||||
const result = (model.specials ?? []).map(it => it.spaceClass).filter(it => it !== undefined)
|
||||
return spaceResult.concat(result as Array<Ref<Class<Space>>>)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
import type { Class, Doc, Mixin, Obj, Ref, Space } from '@anticrm/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin, Resource } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import { AnyComponent } from '@anticrm/ui'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -60,6 +60,8 @@ export interface SpecialNavModel {
|
||||
component: AnyComponent
|
||||
position?: 'top'|'bottom' // undefined == 'top
|
||||
visibleIf?: Resource<(spaces: Space[]) => boolean>
|
||||
// If defined, will be used to find spaces for visibleIf
|
||||
spaceClass?: Ref<Class<Space>>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -142,15 +142,27 @@ export class FullTextIndex implements WithFind {
|
||||
console.log('search', query)
|
||||
const { _id, $search, ...mainQuery } = query
|
||||
if ($search === undefined) return []
|
||||
const docs = await this.adapter.search(_class, query, options?.limit)
|
||||
const ids: Set<Ref<Doc>> = new Set<Ref<Doc>>(docs.map((p) => p.id))
|
||||
for (const doc of docs) {
|
||||
if (doc.attachedTo !== undefined) {
|
||||
ids.add(doc.attachedTo)
|
||||
|
||||
let skip = 0
|
||||
const result: FindResult<T> = []
|
||||
while (true) {
|
||||
const docs = await this.adapter.search(_class, query, options?.limit, skip)
|
||||
if (docs.length === 0) {
|
||||
return result
|
||||
}
|
||||
skip += docs.length
|
||||
const ids: Set<Ref<Doc>> = new Set<Ref<Doc>>(docs.map((p) => p.id))
|
||||
for (const doc of docs) {
|
||||
if (doc.attachedTo !== undefined) {
|
||||
ids.add(doc.attachedTo)
|
||||
}
|
||||
}
|
||||
const resultIds = getResultIds(ids, _id)
|
||||
result.push(...await this.dbStorage.findAll(ctx, _class, { _id: { $in: resultIds }, ...mainQuery }, options))
|
||||
if (result.length > 0 && result.length >= (options?.limit ?? 0)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
const resultIds = getResultIds(ids, _id)
|
||||
return await this.dbStorage.findAll(ctx, _class, { _id: { $in: resultIds }, ...mainQuery }, options)
|
||||
}
|
||||
|
||||
private getFullTextAttributes (clazz: Ref<Class<Obj>>, parentDoc?: Doc): AnyAttribute[] {
|
||||
@ -162,7 +174,7 @@ export class FullTextIndex implements WithFind {
|
||||
}
|
||||
}
|
||||
|
||||
// We also neex to add all mixin attribues if parent is specified.
|
||||
// We also need to add all mixin attribues if parent is specified.
|
||||
if (parentDoc !== undefined) {
|
||||
this.hierarchy
|
||||
.getDescendants(clazz)
|
||||
|
@ -14,7 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Client as MinioClient } from 'minio'
|
||||
import core, {
|
||||
AttachedDoc,
|
||||
Class,
|
||||
@ -26,8 +25,7 @@ import core, {
|
||||
FindOptions,
|
||||
FindResult,
|
||||
Hierarchy,
|
||||
MeasureContext,
|
||||
ModelDb,
|
||||
MeasureContext, ModelDb,
|
||||
Ref,
|
||||
ServerStorage,
|
||||
Storage,
|
||||
@ -38,6 +36,7 @@ import core, {
|
||||
TxFactory,
|
||||
TxResult
|
||||
} from '@anticrm/core'
|
||||
import type { Client as MinioClient } from 'minio'
|
||||
import { FullTextIndex } from './fulltext'
|
||||
import { Triggers } from './triggers'
|
||||
import type { FullTextAdapter, FullTextAdapterFactory } from './types'
|
||||
|
@ -68,7 +68,7 @@ export interface FullTextAdapter {
|
||||
index: (doc: IndexedDoc) => Promise<TxResult>
|
||||
update: (id: Ref<Doc>, update: Record<string, any>) => Promise<TxResult>
|
||||
remove: (id: Ref<Doc>) => Promise<void>
|
||||
search: (_class: Ref<Class<Doc>>, search: DocumentQuery<Doc>, size: number | undefined) => Promise<IndexedDoc[]>
|
||||
search: (_class: Ref<Class<Doc>>, search: DocumentQuery<Doc>, size: number | undefined, from?: number) => Promise<IndexedDoc[]>
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ class ElasticAdapter implements FullTextAdapter {
|
||||
await this.client.close()
|
||||
}
|
||||
|
||||
async search (_class: Ref<Class<Doc>>, query: DocumentQuery<Doc>, size: number | undefined): Promise<IndexedDoc[]> {
|
||||
async search (_class: Ref<Class<Doc>>, query: DocumentQuery<Doc>, size: number | undefined, from: number | undefined): Promise<IndexedDoc[]> {
|
||||
if (query.$search === undefined) return []
|
||||
const search = query.$search.replace(/[\\/+\-=&><!()|{}^"~*&:[\]]/g, '\\$&')
|
||||
|
||||
@ -52,15 +52,27 @@ class ElasticAdapter implements FullTextAdapter {
|
||||
}
|
||||
|
||||
if (query.space != null) {
|
||||
request.bool.should.push({
|
||||
term: {
|
||||
space: {
|
||||
value: query.space,
|
||||
boost: 2.0,
|
||||
case_insensitive: true
|
||||
if (typeof query.space === 'object' && query.space.$in !== undefined) {
|
||||
request.bool.should.push({
|
||||
term: {
|
||||
space: {
|
||||
value: query.space.$in,
|
||||
boost: 2.0,
|
||||
case_insensitive: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
request.bool.should.push({
|
||||
term: {
|
||||
space: {
|
||||
value: query.space,
|
||||
boost: 2.0,
|
||||
case_insensitive: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@ -68,7 +80,8 @@ class ElasticAdapter implements FullTextAdapter {
|
||||
index: this.db,
|
||||
body: {
|
||||
query: request,
|
||||
size: size ?? 200
|
||||
size: size ?? 200,
|
||||
from: from ?? 0
|
||||
}
|
||||
})
|
||||
const hits = result.body.hits.hits as any[]
|
||||
|
@ -122,12 +122,22 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
for (const key in lookup._id) {
|
||||
const as = parent !== undefined ? parent + key : key
|
||||
const value = lookup._id[key]
|
||||
const domain = this.hierarchy.getDomain(value)
|
||||
|
||||
let _class: Ref<Class<Doc>>
|
||||
let attr = 'attachedTo'
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
_class = value[0]
|
||||
attr = value[1]
|
||||
} else {
|
||||
_class = value
|
||||
}
|
||||
const domain = this.hierarchy.getDomain(_class)
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
const step = {
|
||||
from: domain,
|
||||
localField: fullKey,
|
||||
foreignField: 'attachedTo',
|
||||
foreignField: attr,
|
||||
as: as.split('.').join('') + '_lookup'
|
||||
}
|
||||
result.push(step)
|
||||
@ -182,13 +192,22 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
}
|
||||
for (const key in lookup._id) {
|
||||
const value = lookup._id[key]
|
||||
const domain = this.hierarchy.getDomain(value)
|
||||
let _class: Ref<Class<Doc>>
|
||||
let attr = 'attachedTo'
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
_class = value[0]
|
||||
attr = value[1]
|
||||
} else {
|
||||
_class = value
|
||||
}
|
||||
const domain = this.hierarchy.getDomain(_class)
|
||||
const fullKey = parent !== undefined ? parent + key + '_lookup' : key + '_lookup'
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
const arr = object[fullKey]
|
||||
targetObject.$lookup[key] = arr
|
||||
} else {
|
||||
const arr = await this.modelDb.findAll(value, { attachedTo: targetObject._id })
|
||||
const arr = await this.modelDb.findAll(_class, { [attr]: targetObject._id })
|
||||
targetObject.$lookup[key] = arr
|
||||
}
|
||||
}
|
||||
@ -235,7 +254,10 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
pipeline.push({ $limit: options.limit })
|
||||
}
|
||||
const domain = this.hierarchy.getDomain(clazz)
|
||||
const cursor = this.db.collection(domain).aggregate(pipeline)
|
||||
let cursor = this.db.collection(domain).aggregate(pipeline)
|
||||
if (options?.projection !== undefined) {
|
||||
cursor = cursor.project(options.projection)
|
||||
}
|
||||
const result = (await cursor.toArray()) as FindResult<T>
|
||||
for (const row of result) {
|
||||
row.$lookup = {}
|
||||
@ -283,6 +305,10 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
const domain = this.hierarchy.getDomain(_class)
|
||||
let cursor = this.db.collection(domain).find<T>(this.translateQuery(_class, query))
|
||||
|
||||
if (options?.projection !== undefined) {
|
||||
cursor = cursor.project(options.projection)
|
||||
}
|
||||
|
||||
if (options !== null && options !== undefined) {
|
||||
if (options.sort !== undefined) {
|
||||
const sort: Sort = {}
|
||||
|
@ -5,7 +5,7 @@
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"start": "cross-env MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8081 MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin ts-node src/__start.ts",
|
||||
"start": "cross-env MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8081 MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret ts-node src/__start.ts",
|
||||
"build": "heft build",
|
||||
"lint:fix": "eslint --fix src",
|
||||
"bundle": "esbuild src/__start.ts --bundle --platform=node > bundle.js",
|
||||
|
@ -63,14 +63,12 @@ test.describe('recruit tests', () => {
|
||||
|
||||
await page.locator('[id="app-recruit\\:string\\:RecruitApplication"]').click()
|
||||
|
||||
await page.locator('text=Vacancies').hover()
|
||||
await page.click('text=Vacancies Software Engineer >> button')
|
||||
await page.locator('text=Vacancies').click()
|
||||
|
||||
await page.click('[placeholder="Software\\ Engineer"]')
|
||||
await page.fill('[placeholder="Software\\ Engineer"]', 'My vacancy')
|
||||
await page.click('button:has-text("Create")')
|
||||
|
||||
await page.click('span:has-text("My vacancy")')
|
||||
await page.fill('[placeholder="Software\\ Engineer"]', 'My vacancy')
|
||||
await page.click('text=Create Cancel >> button')
|
||||
await page.locator('text=My vacancy').click()
|
||||
|
||||
// Create Applicatio n1
|
||||
await page.click('text=Create')
|
||||
@ -87,6 +85,7 @@ test.describe('recruit tests', () => {
|
||||
|
||||
await page.locator('[id="app-recruit\\:string\\:RecruitApplication"]').click()
|
||||
|
||||
await page.locator('text=Vacancies').click()
|
||||
await page.click('text=Software Engineer')
|
||||
|
||||
await page.click('[name="tooltip-task:string:Kanban"]')
|
||||
@ -102,6 +101,7 @@ test.describe('recruit tests', () => {
|
||||
|
||||
await page.locator('[id="app-recruit\\:string\\:RecruitApplication"]').click()
|
||||
|
||||
await page.locator('text=Vacancies').click()
|
||||
await page.click('text=Software Engineer')
|
||||
|
||||
await expect(page.locator('text=Andrey P.')).toBeVisible()
|
||||
|
Loading…
Reference in New Issue
Block a user