Vacancies redesign (#1088)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-03-04 16:01:46 +07:00 committed by GitHub
parent c06370e44c
commit 25df15e34a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 491 additions and 103 deletions

3
.vscode/launch.json vendored
View File

@ -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,

View File

@ -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'

View File

@ -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>

View File

@ -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
}
}
}

View File

@ -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
}
}
/**

View File

@ -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
}
}

View File

@ -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

View File

@ -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",

View File

@ -49,7 +49,8 @@
"PersonFirstNamePlaceholder": "John",
"PersonLastNamePlaceholder": "Appleseed",
"PersonLocationPlaceholder": "Местоположение",
"ManageVacancyStatuses": "Управление статусами вакансии"
"ManageVacancyStatuses": "Управление статусами вакансии",
"EditVacancy": "Редактировать"
},
"status": {
"CandidateRequired": "Пожалуйста выберите кандидата",

View File

@ -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;

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>&nbsp;{(applications?.get(value._id) ?? 0)}
</div>
</Tooltip>
{/if}

View File

@ -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>&nbsp;{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>

View File

@ -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>&nbsp;{value.name}
</div>
{/if}

View File

@ -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)

View File

@ -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
}
})

View File

@ -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>

View File

@ -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 } })

View File

@ -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
}
}

View File

@ -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>>>)
}

View File

@ -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>>
}
/**

View File

@ -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)

View File

@ -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'

View File

@ -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>
}

View File

@ -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[]

View File

@ -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 = {}

View File

@ -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",

View File

@ -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()