mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-11 04:07:43 +02:00
Filters fix (#1808)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
@@ -103,7 +103,7 @@ export class TStatus extends TAttachedDoc implements Status {
|
||||
}
|
||||
|
||||
@Model(contact.class.Employee, contact.class.Person)
|
||||
@UX(contact.string.Employee, contact.icon.Person)
|
||||
@UX(contact.string.Employee, contact.icon.Person, undefined, 'name')
|
||||
export class TEmployee extends TPerson implements Employee {
|
||||
@Prop(Collection(contact.class.Status), contact.string.Status)
|
||||
statuses?: number
|
||||
|
||||
@@ -75,7 +75,7 @@ export class TVacancy extends TSpaceWithStates implements Vacancy {
|
||||
export class TCandidates extends TSpace implements Candidates {}
|
||||
|
||||
@Mixin(recruit.mixin.Candidate, contact.class.Person)
|
||||
@UX(recruit.string.Candidate, recruit.icon.RecruitApplication)
|
||||
@UX(recruit.string.Candidate, recruit.icon.RecruitApplication, undefined, 'name')
|
||||
export class TCandidate extends TPerson implements Candidate {
|
||||
@Prop(TypeString(), recruit.string.Title)
|
||||
@Index(IndexKind.FullText)
|
||||
@@ -452,7 +452,7 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['title', 'source', 'city', 'modifiedOn']
|
||||
filters: ['title', 'source', 'city', 'modifiedOn', 'onsite', 'remote']
|
||||
})
|
||||
|
||||
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ClassFilters, {
|
||||
|
||||
@@ -501,6 +501,10 @@ export function createModel (builder: Builder): void {
|
||||
component: view.component.ValueFilter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.TypeBoolean, core.class.Class, view.mixin.AttributeFilter, {
|
||||
component: view.component.ValueFilter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.TypeNumber, core.class.Class, view.mixin.AttributeFilter, {
|
||||
component: view.component.ValueFilter
|
||||
})
|
||||
|
||||
@@ -39,13 +39,15 @@ const predicates: Record<string, PredicateFactory> = {
|
||||
if (!Array.isArray(o)) {
|
||||
throw new Error('$in predicate requires array')
|
||||
}
|
||||
return (docs) => execPredicate(docs, propertyKey, (value) => o.includes(value))
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return (docs) => execPredicate(docs, propertyKey, (value) => o.some((p) => p == value))
|
||||
},
|
||||
$nin: (o, propertyKey) => {
|
||||
if (!Array.isArray(o)) {
|
||||
throw new Error('$nin predicate requires array')
|
||||
}
|
||||
return (docs) => execPredicate(docs, propertyKey, (value) => !o.includes(value))
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return (docs) => execPredicate(docs, propertyKey, (value) => !o.some((p) => p == value))
|
||||
},
|
||||
|
||||
$like: (query: string, propertyKey: string): Predicate => {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import { Account, Class, Client, Doc, generateId, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
|
||||
import { Card, createQuery, getClient, SpaceSelector, UserBox } from '@anticrm/presentation'
|
||||
import type { Applicant, Candidate, Vacancy } from '@anticrm/recruit'
|
||||
import type { Applicant, Candidate } from '@anticrm/recruit'
|
||||
import task, { calcRank, SpaceWithStates, State } from '@anticrm/task'
|
||||
import ui, {
|
||||
Button,
|
||||
@@ -41,8 +41,9 @@
|
||||
export let preserveCandidate = false
|
||||
|
||||
let status: Status = OK
|
||||
let createMore: boolean = false
|
||||
|
||||
const doc: Applicant = {
|
||||
let doc: Applicant = {
|
||||
state: '' as Ref<State>,
|
||||
doneState: null,
|
||||
number: 0,
|
||||
@@ -111,6 +112,25 @@
|
||||
rank: calcRank(lastOne, undefined)
|
||||
}
|
||||
)
|
||||
|
||||
if (createMore) {
|
||||
// Prepare for next
|
||||
doc = {
|
||||
state: selectedState._id,
|
||||
doneState: null,
|
||||
number: 0,
|
||||
assignee: assignee,
|
||||
rank: '',
|
||||
attachedTo: candidate,
|
||||
attachedToClass: recruit.mixin.Candidate,
|
||||
_class: recruit.class.Applicant,
|
||||
space: space,
|
||||
_id: generateId(),
|
||||
collection: 'applications',
|
||||
modifiedOn: Date.now(),
|
||||
modifiedBy: '' as Ref<Account>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function invokeValidate (
|
||||
@@ -134,20 +154,6 @@
|
||||
|
||||
$: validate(doc, doc._class)
|
||||
|
||||
let selectedVacancy: Vacancy
|
||||
let selectedCandidate: Person
|
||||
const vacancyQuery = createQuery()
|
||||
$: if (doc.space !== undefined) {
|
||||
vacancyQuery.query(recruit.class.Vacancy, { _id: doc.space }, (result) => {
|
||||
selectedVacancy = result[0]
|
||||
})
|
||||
}
|
||||
const candidateQuery = createQuery()
|
||||
$: if (doc.attachedTo !== undefined) {
|
||||
candidateQuery.query(contact.class.Person, { _id: doc.attachedTo as Ref<Person> }, (result) => {
|
||||
selectedCandidate = result[0]
|
||||
})
|
||||
}
|
||||
let states: Array<{ id: number | string; color: number; label: string }> = []
|
||||
let selectedState: State
|
||||
const statesQuery = createQuery()
|
||||
@@ -164,6 +170,8 @@
|
||||
},
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
} else {
|
||||
states = []
|
||||
}
|
||||
const manager = createFocusManager()
|
||||
|
||||
@@ -192,7 +200,7 @@
|
||||
label={recruit.string.CreateApplication}
|
||||
okAction={createApplication}
|
||||
canSave={status.severity === Severity.OK}
|
||||
createMore={false}
|
||||
bind:createMore
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
@@ -211,61 +219,63 @@
|
||||
</svelte:fragment>
|
||||
<StatusControl slot="error" {status} />
|
||||
<svelte:fragment slot="pool">
|
||||
{#if !preserveCandidate}
|
||||
{#key doc}
|
||||
{#if !preserveCandidate}
|
||||
<UserBox
|
||||
focusIndex={1}
|
||||
_class={contact.class.Person}
|
||||
options={{ sort: { modifiedOn: -1 } }}
|
||||
excluded={existingApplicants}
|
||||
label={recruit.string.Candidate}
|
||||
placeholder={recruit.string.Candidates}
|
||||
bind:value={doc.attachedTo}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
create={{ component: recruit.component.CreateCandidate, label: recruit.string.CreateCandidate }}
|
||||
/>
|
||||
{/if}
|
||||
<UserBox
|
||||
focusIndex={1}
|
||||
_class={contact.class.Person}
|
||||
options={{ sort: { modifiedOn: -1 } }}
|
||||
excluded={existingApplicants}
|
||||
label={recruit.string.Candidate}
|
||||
placeholder={recruit.string.Candidates}
|
||||
bind:value={doc.attachedTo}
|
||||
focusIndex={2}
|
||||
_class={contact.class.Employee}
|
||||
label={recruit.string.AssignRecruiter}
|
||||
placeholder={recruit.string.Recruiters}
|
||||
bind:value={doc.assignee}
|
||||
allowDeselect
|
||||
titleDeselect={recruit.string.UnAssignRecruiter}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
create={{ component: recruit.component.CreateCandidate, label: recruit.string.CreateCandidate }}
|
||||
/>
|
||||
{/if}
|
||||
<UserBox
|
||||
focusIndex={2}
|
||||
_class={contact.class.Employee}
|
||||
label={recruit.string.AssignRecruiter}
|
||||
placeholder={recruit.string.Recruiters}
|
||||
bind:value={doc.assignee}
|
||||
allowDeselect
|
||||
titleDeselect={recruit.string.UnAssignRecruiter}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
/>
|
||||
{#if states && doc.space}
|
||||
<Button
|
||||
focusIndex={3}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
ColorPopup,
|
||||
{ value: states, searchable: true, placeholder: ui.string.SearchDots },
|
||||
eventToHTMLElement(ev),
|
||||
(result) => {
|
||||
if (result && result.id) {
|
||||
doc.state = result.id
|
||||
selectedState = result
|
||||
selectedState.title = result.label
|
||||
{#if states && doc.space}
|
||||
<Button
|
||||
focusIndex={3}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
ColorPopup,
|
||||
{ value: states, searchable: true, placeholder: ui.string.SearchDots },
|
||||
eventToHTMLElement(ev),
|
||||
(result) => {
|
||||
if (result && result.id) {
|
||||
doc.state = result.id
|
||||
selectedState = result
|
||||
selectedState.title = result.label
|
||||
}
|
||||
manager.setFocusPos(3)
|
||||
}
|
||||
manager.setFocusPos(3)
|
||||
}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div slot="content" class="flex-row-center">
|
||||
{#if selectedState}
|
||||
<div class="color" style="background-color: {getPlatformColor(selectedState.color)}" />
|
||||
<span class="label overflow-label">{selectedState.title}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div slot="content" class="flex-row-center">
|
||||
{#if selectedState}
|
||||
<div class="color" style="background-color: {getPlatformColor(selectedState.color)}" />
|
||||
<span class="label overflow-label">{selectedState.title}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
{/key}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentQuery, getObjectValue, Ref } from '@anticrm/core'
|
||||
import { Class, Doc, DocumentQuery, getObjectValue, Ref, RefTo } from '@anticrm/core'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import presentation, { getClient } from '@anticrm/presentation'
|
||||
import { CheckBox } from '@anticrm/ui'
|
||||
import ui, { CheckBox, Label } from '@anticrm/ui'
|
||||
import { Filter } from '@anticrm/view'
|
||||
import { onMount } from 'svelte'
|
||||
import { buildConfigLookup, getPresenter } from '../../utils'
|
||||
@@ -57,33 +57,37 @@
|
||||
}
|
||||
]
|
||||
|
||||
let values: Set<Doc> = new Set<Doc>()
|
||||
let values: (Doc | undefined | null)[] = []
|
||||
|
||||
$: getValues(search)
|
||||
|
||||
const targets = new Map<any, number>()
|
||||
async function getValues (search: string): Promise<void> {
|
||||
const attrib = await promise
|
||||
const resultQuery =
|
||||
search !== ''
|
||||
? {
|
||||
[attrib.sortingKey]: { $like: '%' + search + '%' },
|
||||
...query
|
||||
}
|
||||
: query
|
||||
const res = await client.findAll(_class, resultQuery, { lookup })
|
||||
const objects = []
|
||||
const set: Set<any> = new Set<any>()
|
||||
for (const obj of res) {
|
||||
const value = (obj as any)[filter.key.key]
|
||||
if (set.has(value)) continue
|
||||
objects.push(obj)
|
||||
set.add(value)
|
||||
targets.clear()
|
||||
const baseObjects = await client.findAll(_class, query, { projection: { [filter.key.key]: 1 } })
|
||||
for (const object of baseObjects) {
|
||||
const value = getObjectValue(filter.key.key, object) ?? undefined
|
||||
targets.set(value, (targets.get(value) ?? 0) + 1)
|
||||
}
|
||||
const targetClass = (hierarchy.getAttribute(_class, filter.key.key).type as RefTo<Doc>).to
|
||||
const clazz = hierarchy.getClass(targetClass)
|
||||
const resultQuery =
|
||||
search !== '' && clazz.sortingKey
|
||||
? {
|
||||
[clazz.sortingKey]: { $like: '%' + search + '%' },
|
||||
_id: { $in: Array.from(targets.keys()) }
|
||||
}
|
||||
: {
|
||||
_id: { $in: Array.from(targets.keys()) }
|
||||
}
|
||||
values = await client.findAll(targetClass, resultQuery, { projection: { _id: 1 } })
|
||||
if (targets.has(undefined)) {
|
||||
values.unshift(undefined)
|
||||
}
|
||||
values = new Set(objects.map((obj) => getObjectValue(tkey, obj)))
|
||||
}
|
||||
|
||||
function isSelected (value: Doc, values: any[]): boolean {
|
||||
return values.includes(value?._id ?? '')
|
||||
function isSelected (value: Doc | undefined | null, values: any[]): boolean {
|
||||
return values.includes(value?._id ?? value)
|
||||
}
|
||||
|
||||
function checkMode () {
|
||||
@@ -92,11 +96,15 @@
|
||||
filter.mode = newMode !== undefined ? newMode : filter.mode
|
||||
}
|
||||
|
||||
function toggle (value: Doc): void {
|
||||
function toggle (value: Doc | undefined | null): void {
|
||||
if (isSelected(value, filter.value)) {
|
||||
filter.value = filter.value.filter((p) => p !== value._id)
|
||||
filter.value = filter.value.filter((p) => (value ? p !== value._id : p != null))
|
||||
} else {
|
||||
filter.value = [...filter.value, value._id]
|
||||
if (value) {
|
||||
filter.value = [...filter.value, value._id]
|
||||
} else {
|
||||
filter.value = [...filter.value, undefined]
|
||||
}
|
||||
}
|
||||
checkMode()
|
||||
onChange(filter)
|
||||
@@ -128,17 +136,28 @@
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#await promise then attribute}
|
||||
{#each Array.from(values) as value}
|
||||
{#each values as value}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
toggle(value)
|
||||
}}
|
||||
>
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(value, filter.value)} primary />
|
||||
<div class="flex-between w-full">
|
||||
<div class="flex">
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(value, filter.value)} primary />
|
||||
</div>
|
||||
{#if value}
|
||||
<svelte:component this={attribute.presenter} {value} {...attribute.props} />
|
||||
{:else}
|
||||
<Label label={ui.string.NotSelected} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="content-trans-color">
|
||||
{targets.get(value?._id)}
|
||||
</div>
|
||||
</div>
|
||||
<svelte:component this={attribute.presenter} {value} {...attribute.props} />
|
||||
</button>
|
||||
{/each}
|
||||
{/await}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { Class, Doc, DocumentQuery, getObjectValue, Ref } from '@anticrm/core'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import presentation, { getClient } from '@anticrm/presentation'
|
||||
import { CheckBox } from '@anticrm/ui'
|
||||
import ui, { CheckBox, Label } from '@anticrm/ui'
|
||||
import { Filter } from '@anticrm/view'
|
||||
import { onMount } from 'svelte'
|
||||
import { getPresenter } from '../../utils'
|
||||
@@ -55,11 +55,15 @@
|
||||
const key = { key: filter.key.key }
|
||||
const promise = getPresenter(client, _class, key, key)
|
||||
|
||||
let values: Set<any> = new Set<any>()
|
||||
let values = new Map<any, number>()
|
||||
let selectedValues: Set<any> = new Set<any>()
|
||||
const realValues = new Map<any, Set<any>>()
|
||||
|
||||
$: getValues(search)
|
||||
|
||||
async function getValues (search: string): Promise<void> {
|
||||
values.clear()
|
||||
realValues.clear()
|
||||
const resultQuery =
|
||||
search !== ''
|
||||
? {
|
||||
@@ -67,12 +71,18 @@
|
||||
...query
|
||||
}
|
||||
: query
|
||||
const res = await client.findAll(_class, resultQuery)
|
||||
values = new Set(res.map((obj) => getObjectValue(filter.key.key, obj)))
|
||||
const res = await client.findAll(_class, resultQuery, { projection: { [filter.key.key]: 1 } })
|
||||
for (const object of res) {
|
||||
const realValue = getObjectValue(filter.key.key, object)
|
||||
const value = typeof realValue === 'string' ? realValue.trim().toUpperCase() : realValue ?? undefined
|
||||
values.set(value, (values.get(value) ?? 0) + 1)
|
||||
realValues.set(value, (realValues.get(value) ?? new Set()).add(realValue))
|
||||
}
|
||||
values = values
|
||||
}
|
||||
|
||||
function isSelected (value: any, values: any[]): boolean {
|
||||
return values.includes(value)
|
||||
function isSelected (value: any, values: Set<any>): boolean {
|
||||
return values.has(value)
|
||||
}
|
||||
|
||||
function checkMode () {
|
||||
@@ -82,11 +92,15 @@
|
||||
}
|
||||
|
||||
function toggle (value: any): void {
|
||||
if (isSelected(value, filter.value)) {
|
||||
filter.value = filter.value.filter((p) => p !== value)
|
||||
if (isSelected(value, selectedValues)) {
|
||||
selectedValues.delete(value)
|
||||
} else {
|
||||
filter.value = [...filter.value, value]
|
||||
selectedValues.add(value)
|
||||
}
|
||||
selectedValues = selectedValues
|
||||
filter.value = Array.from(selectedValues.values())
|
||||
.map((p) => Array.from(realValues.get(p) ?? []))
|
||||
.flat()
|
||||
checkMode()
|
||||
onChange(filter)
|
||||
}
|
||||
@@ -117,17 +131,28 @@
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#await promise then attribute}
|
||||
{#each Array.from(values) as value}
|
||||
{#each Array.from(values.keys()) as value}
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click={() => {
|
||||
toggle(value)
|
||||
}}
|
||||
>
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(value, filter.value)} primary />
|
||||
<div class="flex-between w-full">
|
||||
<div class="flex">
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(value, selectedValues)} primary />
|
||||
</div>
|
||||
{#if value}
|
||||
<svelte:component this={attribute.presenter} {value} {...attribute.props} />
|
||||
{:else}
|
||||
<Label label={ui.string.NotSelected} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="content-trans-color">
|
||||
{values.get(value)}
|
||||
</div>
|
||||
</div>
|
||||
<svelte:component this={attribute.presenter} {value} {...attribute.props} />
|
||||
</button>
|
||||
{/each}
|
||||
{/await}
|
||||
|
||||
Reference in New Issue
Block a user