UBER-148: My Applications in recruit (#3235)

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
Vyacheslav Tumanov 2023-05-23 15:19:35 +05:00 committed by GitHub
parent 8b6e304ceb
commit e164e1c9af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 152 additions and 57 deletions

View File

@ -240,7 +240,7 @@ export function createModel (builder: Builder): void {
const skillsId = 'skills'
const candidatesId = 'candidates'
const archiveId = 'archive'
const assignedId = 'assigned'
const myApplicationsId = 'my-applications'
const organizationsId = 'organizations'
builder.createDoc(
@ -324,8 +324,8 @@ export function createModel (builder: Builder): void {
position: 'bottom'
},
{
id: assignedId,
label: task.string.AssignedToMe,
id: myApplicationsId,
label: recruit.string.MyApplications,
icon: recruit.icon.AssignedToMe,
component: task.component.AssignedTasks,
position: 'event',
@ -941,7 +941,7 @@ export function createModel (builder: Builder): void {
createGotoSpecialAction(builder, talentsId, 'g->e', recruit.string.GotoTalents)
createGotoSpecialAction(builder, vacanciesId, 'g->v', recruit.string.GotoVacancies)
createGotoSpecialAction(builder, skillsId, 'g->s', recruit.string.GotoSkills)
createGotoSpecialAction(builder, assignedId, 'g->h', recruit.string.GotoAssigned)
createGotoSpecialAction(builder, myApplicationsId, 'g->h', recruit.string.GotoMyApplications)
createGotoSpecialAction(builder, candidatesId, 'g->a', recruit.string.GotoApplicants)
createAction(builder, {

View File

@ -58,7 +58,7 @@ export default mergeIds(recruitId, recruit, {
GotoTalents: '' as IntlString,
GotoVacancies: '' as IntlString,
GotoSkills: '' as IntlString,
GotoAssigned: '' as IntlString,
GotoMyApplications: '' as IntlString,
GotoApplicants: '' as IntlString,
GotoRecruitApplication: '' as IntlString,
VacancyList: '' as IntlString,

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { TabList } from '@hcengineering/ui'
import TabList from './TabList.svelte'
import { IModeSelector } from '../utils'
export let props: IModeSelector

View File

@ -181,6 +181,7 @@ export { default as Wizard } from './components/wizard/Wizard.svelte'
export { default as StepsDialog } from './components/StepsDialog.svelte'
export { default as EmojiPopup } from './components/EmojiPopup.svelte'
export { default as IconWithEmojii } from './components/IconWithEmojii.svelte'
export { default as ModeSelector } from './components/ModeSelector.svelte'
export * from './types'
export * from './location'

View File

@ -15,7 +15,7 @@
import { generateId } from '@hcengineering/core'
import type { Metadata } from '@hcengineering/platform'
import { setMetadata } from '@hcengineering/platform'
import { IntlString, setMetadata } from '@hcengineering/platform'
import autolinker from 'autolinker'
import { writable } from 'svelte/store'
import { Notification, NotificationPosition, NotificationSeverity, notificationsStore } from '.'
@ -169,3 +169,12 @@ export function replaceURLs (text: string): string {
stripPrefix: false
})
}
/**
* @public
*/
export interface IModeSelector {
mode: string
config: Array<[string, IntlString, object]>
onChange: (_mode: string) => void
}

View File

@ -84,7 +84,7 @@
"GotoTalents": "Go to Talents",
"GotoVacancies": "Go to Vacancies",
"GotoSkills": "Go to Skills",
"GotoAssigned": "Go to my Assigned",
"GotoMyApplications": "Go to My Applications",
"GotoApplicants": "Go to Applications",
"GotoRecruitApplication": "Switch to Recruit Application",
"AddDropHere": "Add or drop resume",
@ -112,7 +112,8 @@
"OpenVacancyList": "Open list",
"Export": "Export",
"ConfigLabel": "Recruiting",
"ConfigDescription": "Extension to manage Talents/Applicants and Vacancies."
"ConfigDescription": "Extension to manage Talents/Applicants and Vacancies.",
"MyApplications": "My applications"
},
"status": {
"TalentRequired": "Please select talent",

View File

@ -86,7 +86,7 @@
"GotoTalents": "Перейте к талантам",
"GotoVacancies": "Перейти к вакансиям",
"GotoSkills": "Перейти к навыкам",
"GotoAssigned": "Перейти к моим назначениям",
"GotoMyApplications": "Перейти к Моим Кандидатам",
"GotoApplicants": "Перейти к кандидатам",
"GotoRecruitApplication": "Перейти к Приложению Рекрутинг",
"AddDropHere": "Добавить или перетянуть резюме",
@ -112,7 +112,8 @@
"OpenVacancyList": "Открыть список",
"Export": "Экспорт",
"ConfigLabel": "Рекрутинг",
"ConfigDescription": "Модуль по работе с талантами/кандидатами и вакансиями"
"ConfigDescription": "Модуль по работе с талантами/кандидатами и вакансиями",
"MyApplications": "Мои кандидаты"
},
"status": {
"TalentRequired": "Пожалуйста выберите таланта",

View File

@ -124,6 +124,7 @@ export default mergeIds(recruitId, recruit, {
PerformMatch: '' as IntlString,
MoveApplication: '' as IntlString,
Application: '' as IntlString,
MyApplications: '' as IntlString,
TemplateReplace: '' as IntlString,
TemplateReplaceConfirm: '' as IntlString,

View File

@ -14,29 +14,53 @@
-->
<script lang="ts">
import { EmployeeAccount } from '@hcengineering/contact'
import { Class, DocumentQuery, getCurrentAccount, Ref } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import tags, { selectedTagElements, TagCategory, TagElement } from '@hcengineering/tags'
import { DoneState, Task } from '@hcengineering/task'
import { Component, Label, SearchEdit } from '@hcengineering/ui'
import { TableBrowser } from '@hcengineering/view-resources'
import { Component, IModeSelector, Label, resolvedLocationStore, SearchEdit, ModeSelector } from '@hcengineering/ui'
import {
activeViewlet,
FilterButton,
getViewOptions,
makeViewletKey,
TableBrowser,
updateActiveViewlet,
viewOptionStore
} from '@hcengineering/view-resources'
import task from '../plugin'
import { IntlString } from '@hcengineering/platform'
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
import view, { Viewlet } from '@hcengineering/view'
import { onDestroy } from 'svelte'
import FilterBar from '@hcengineering/view-resources/src/components/filter/FilterBar.svelte'
export let _class: Ref<Class<Task>> = task.class.Task
export let labelTasks = task.string.Tasks
let search = ''
let resultQuery: DocumentQuery<Task> = {}
const currentUser = getCurrentAccount() as EmployeeAccount
const assigned = { assignee: currentUser.employee }
const created = { createdBy: currentUser._id }
let subscribed = { _id: { $in: [] as Ref<Task>[] } }
$: baseQuery = updateBaseQuery(mode, { assigned, created, subscribed })
function updateBaseQuery (mode: string, queries: { [key: string]: DocumentQuery<Task> }) {
return { ...queries[mode] }
}
let searchQuery: DocumentQuery<Task> = { ...baseQuery }
function updateSearchQuery (search: string): void {
searchQuery = search === '' ? { ...baseQuery } : { ...baseQuery, $search: search }
}
$: if (baseQuery) updateSearchQuery(search)
$: resultQuery = { ...searchQuery }
const client = getClient()
const currentUser = getCurrentAccount() as EmployeeAccount
let category: Ref<TagCategory> | undefined = undefined
let documentIds: Ref<Task>[] = []
function updateResultQuery (search: string, documentIds: Ref<Task>[], doneStates: DoneState[]): void {
resultQuery = search === '' ? {} : { $search: search }
resultQuery.assignee = currentUser.employee
resultQuery.doneState = { $nin: doneStates.map((it) => it._id) }
if (documentIds.length > 0) {
resultQuery._id = { $in: documentIds }
@ -60,8 +84,57 @@
).values()
)
})
const subscribedQuery = createQuery()
function getSubscribed () {
subscribedQuery.query(
_class,
{ 'notification:mixin:Collaborators.collaborators': getCurrentAccount()._id },
(result) => {
const newSub = result.map((p) => p._id as Ref<Doc> as Ref<Task>)
const curSub = subscribed._id.$in
if (curSub.length !== newSub.length || curSub.some((id, i) => newSub[i] !== id)) {
subscribed = { _id: { $in: newSub } }
}
},
{ sort: { _id: 1 } }
)
}
$: if (mode === 'subscribed') getSubscribed()
const config: [string, IntlString, object][] = [
['assigned', view.string.Assigned, {}],
['created', view.string.Created, {}],
['subscribed', view.string.Subscribed, {}]
]
let [[mode]] = config
function handleChangeMode (newMode: string) {
if (newMode === mode) return
mode = newMode
}
$: modeSelectorProps = {
config,
mode,
onChange: handleChangeMode
} as IModeSelector
$: updateResultQuery(search, documentIds, doneStates)
let viewlets: WithLookup<Viewlet>[] | undefined
let key = makeViewletKey()
onDestroy(
resolvedLocationStore.subscribe((loc) => {
key = makeViewletKey(loc)
})
)
$: viewlet = viewlets && updateActiveViewlet(viewlets, active)
$: active = $activeViewlet[key]
const viewletQuery = createQuery()
viewletQuery.query(view.class.Viewlet, { attachTo: _class }, (res) => (viewlets = res), {
lookup: {
descriptor: view.class.ViewletDescriptor
}
})
$: viewOptions = getViewOptions(viewlet, $viewOptionStore)
function updateCategory (detail: { category: Ref<TagCategory> | null; elements: TagElement[] }) {
category = detail.category ?? undefined
@ -70,18 +143,34 @@
const handleChange = (evt: any) => updateCategory(evt.detail)
</script>
<div class="ac-header full divide caption-height">
<div
class="ac-header full divide"
class:header-with-mode-selector={modeSelectorProps !== undefined}
class:header-without-label={!labelTasks}
>
<div class="ac-header__wrap-title">
<span class="ac-header__title"><Label label={labelTasks} /></span>
{#if modeSelectorProps !== undefined}
<ModeSelector props={modeSelectorProps} />
{/if}
</div>
<SearchEdit
bind:value={search}
on:change={() => {
updateResultQuery(search, documentIds, doneStates)
}}
/>
</div>
<div class="ac-header full divide search-start">
<div class="ac-header-full small-gap">
<SearchEdit
bind:value={search}
on:change={() => {
updateResultQuery(search, documentIds, doneStates)
}}
/>
<div class="buttons-divider" />
<FilterButton {_class} />
</div>
{#if viewlet}
<ViewletSettingButton bind:viewOptions {viewlet} />
{/if}
</div>
<FilterBar {_class} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<Component is={tags.component.TagsCategoryBar} props={{ targetClass: _class, category }} on:change={handleChange} />

View File

@ -146,9 +146,7 @@
"GotoMyIssues": "Go to my issues",
"GotoTrackerApplication": "Switch to Tracker Application",
"Assigned": "Assigned",
"Created": "Created",
"Subscribed": "Subscribed",
"CreatedOne": "Created",
"MoveIssues": "Move issues",
"MoveIssuesDescription": "Select the project you want to move issues to.",

View File

@ -146,9 +146,7 @@
"GotoMyIssues": "Перейи к моим задачам",
"GotoTrackerApplication": "Перейти к приложению Трекер",
"Assigned": "Назначенные",
"Created": "{value, plural, =1 {Создана} other {Созданные}}",
"Subscribed": "Отслеживаемые",
"CreatedOne": "Создана",
"MoveIssues": "Переместить задачи",
"MoveIssuesDescription": "Выберите проект, в который вы хотите переместить задачи.",

View File

@ -396,7 +396,7 @@
await subIssuesComponent.save(parents, _id)
addNotification(await translate(tracker.string.IssueCreated, {}), getTitle(object.title), IssueNotification, {
issueId: _id,
subTitlePostfix: (await translate(tracker.string.Created, { value: 1 })).toLowerCase(),
subTitlePostfix: (await translate(tracker.string.CreatedOne, {})).toLowerCase(),
issueUrl: currentProject && generateIssueShortLink(getIssueId(currentProject, value as Issue))
})

View File

@ -19,7 +19,7 @@
import IssuesView from './IssuesView.svelte'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { IModeSelector } from '../../utils'
import { IModeSelector } from '@hcengineering/ui'
export let currentSpace: Ref<Project> | undefined = undefined
export let baseQuery: DocumentQuery<Issue> = {}

View File

@ -1,12 +1,10 @@
<script lang="ts">
import { Ref, Space } from '@hcengineering/core'
import { TabList, SearchEdit } from '@hcengineering/ui'
import { TabList, SearchEdit, IModeSelector, ModeSelector } from '@hcengineering/ui'
import { Viewlet } from '@hcengineering/view'
import { FilterButton, setActiveViewletId } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import { WithLookup } from '@hcengineering/core'
import ModeSelector from '../ModeSelector.svelte'
import { IModeSelector } from '../../utils'
export let space: Ref<Space> | undefined = undefined
export let viewlet: WithLookup<Viewlet> | undefined

View File

@ -3,7 +3,7 @@
import { IntlString, translate } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { Button, IconDetails, IconDetailsFilled, resolvedLocationStore } from '@hcengineering/ui'
import { Button, IconDetails, IconDetailsFilled, IModeSelector, resolvedLocationStore } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import {
FilterBar,
@ -18,7 +18,6 @@
import tracker from '../../plugin'
import IssuesContent from './IssuesContent.svelte'
import IssuesHeader from './IssuesHeader.svelte'
import { IModeSelector } from '../../utils'
export let space: Ref<Space> | undefined = undefined
export let query: DocumentQuery<Issue> = {}

View File

@ -21,12 +21,13 @@
import tracker from '../../plugin'
import IssuesView from '../issues/IssuesView.svelte'
import { IModeSelector } from '../../utils'
import { IModeSelector } from '@hcengineering/ui'
import view from '@hcengineering/view'
const config: [string, IntlString, object][] = [
['assigned', tracker.string.Assigned, {}],
['created', tracker.string.Created, { value: 2 }],
['subscribed', tracker.string.Subscribed, {}]
['assigned', view.string.Assigned, {}],
['created', view.string.Created, {}],
['subscribed', view.string.Subscribed, {}]
]
const currentUser = getCurrentAccount() as EmployeeAccount
const assigned = { assignee: currentUser.employee }

View File

@ -193,9 +193,7 @@ export default mergeIds(trackerId, tracker, {
AllFilters: '' as IntlString,
NoDescription: '' as IntlString,
Assigned: '' as IntlString,
Created: '' as IntlString,
Subscribed: '' as IntlString,
CreatedOne: '' as IntlString,
Relations: '' as IntlString,
RemoveRelation: '' as IntlString,

View File

@ -650,12 +650,3 @@ export function issueToAttachedData (issue: Issue): AttachedData<Issue> {
const { _id, _class, space, ...data } = issue
return { ...data }
}
/**
* @public
*/
export interface IModeSelector {
mode: string
config: Array<[string, IntlString, object]>
onChange: (_mode: string) => void
}

View File

@ -28,6 +28,10 @@
"SelectItemAll": "Select all items",
"SelectItemNone": "Deselect all items",
"Assigned": "Assigned",
"Created": "Created",
"Subscribed": "Subscribed",
"ShowPreview": "Show document preview",
"ShowActions": "Show actions popup",

View File

@ -31,6 +31,9 @@
"SelectItem": "Выбрать",
"SelectItemAll": "Выбрать все",
"SelectItemNone": "Снять все выделения",
"Assigned": "Назначенные",
"Created": "Созданные",
"Subscribed": "Отслеживаемые",
"ShowPreview": "Предпросмотр документа",
"ShowActions": "Показать действия",
"RestoreDefaults": "По умолчанию",

View File

@ -725,7 +725,10 @@ const view = plugin(viewId, {
AddSavedView: '' as IntlString,
Timeline: '' as IntlString,
Public: '' as IntlString,
Hide: '' as IntlString
Hide: '' as IntlString,
Assigned: '' as IntlString,
Created: '' as IntlString,
Subscribed: '' as IntlString
},
icon: {
Table: '' as Asset,

View File

@ -14,7 +14,7 @@ test.describe('workbench tests', () => {
await page.click('[id="app-recruit\\:string\\:RecruitApplication"]')
await expect(page).toHaveURL(`${PlatformURI}/workbench/sanity-ws/recruit`)
// Click text=Applications
await page.click('text=Applications')
await page.click('text=/^Applications/')
await expect(page).toHaveURL(`${PlatformURI}/workbench/sanity-ws/recruit/candidates`)
// Click text=Applications Application >> span
await expect(page.locator('text=Applications >> nth=1')).toBeVisible()