Filters fix (#1808)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov
2022-05-20 14:17:57 +07:00
committed by GitHub
parent fcea07cba5
commit 06867083d1
7 changed files with 174 additions and 114 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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, {
+4
View File
@@ -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
})
+4 -2
View File
@@ -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}