Various small issue fixes (#2556)

* Fix Issue title be selectable
* TSK-582: Fix Vacancy subtitle be hyperlink
* TSK-585: Add tasks to Applications

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-01-29 12:44:12 +07:00 committed by GitHub
parent 317e705b39
commit d1fb81582d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 348 additions and 69 deletions

View File

@ -44,6 +44,7 @@
"@hcengineering/setting": "^0.6.2", "@hcengineering/setting": "^0.6.2",
"@hcengineering/model-task": "^0.6.0", "@hcengineering/model-task": "^0.6.0",
"@hcengineering/workbench": "^0.6.2", "@hcengineering/workbench": "^0.6.2",
"@hcengineering/model-tracker": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0", "@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-calendar": "^0.6.0", "@hcengineering/model-calendar": "^0.6.0",
"@hcengineering/model-tags": "^0.6.0", "@hcengineering/model-tags": "^0.6.0",

View File

@ -39,6 +39,7 @@ import core, { TAttachedDoc, TSpace } from '@hcengineering/model-core'
import presentation from '@hcengineering/model-presentation' import presentation from '@hcengineering/model-presentation'
import tags from '@hcengineering/model-tags' import tags from '@hcengineering/model-tags'
import task, { actionTemplates, DOMAIN_TASK, TSpaceWithStates, TTask } from '@hcengineering/model-task' import task, { actionTemplates, DOMAIN_TASK, TSpaceWithStates, TTask } from '@hcengineering/model-task'
import tracker from '@hcengineering/model-tracker'
import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view' import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view'
import workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench' import workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench'
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform' import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
@ -331,6 +332,11 @@ export function createModel (builder: Builder): void {
'city', 'city',
'applications', 'applications',
'attachments', 'attachments',
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Relations
},
'comments', 'comments',
{ {
// key: '$lookup.skills', // Required, since presenter require list of tag references or '' and TagsPopupPresenter // key: '$lookup.skills', // Required, since presenter require list of tag references or '' and TagsPopupPresenter
@ -351,7 +357,14 @@ export function createModel (builder: Builder): void {
sortingKey: ['$lookup.channels.lastMessage', 'channels'] sortingKey: ['$lookup.channels.lastMessage', 'channels']
} }
], ],
hiddenKeys: ['name'] hiddenKeys: ['name'],
options: {
lookup: {
_id: {
related: [tracker.class.Issue, 'relations._id']
}
}
}
}, },
recruit.viewlet.TableCandidate recruit.viewlet.TableCandidate
) )
@ -390,8 +403,21 @@ export function createModel (builder: Builder): void {
descriptor: task.viewlet.StatusTable, descriptor: task.viewlet.StatusTable,
config: [ config: [
'', '',
'attachedTo', {
key: 'attachedTo',
presenter: contact.component.PersonRefPresenter,
sortingKey: 'attachedTo',
label: recruit.string.Talent,
props: {
_class: recruit.mixin.Candidate
}
},
'assignee', 'assignee',
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Issues
},
'state', 'state',
'doneState', 'doneState',
'attachments', 'attachments',
@ -399,9 +425,18 @@ export function createModel (builder: Builder): void {
'modifiedOn', 'modifiedOn',
{ {
key: '$lookup.attachedTo.$lookup.channels', key: '$lookup.attachedTo.$lookup.channels',
label: contact.string.ContactInfo,
sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels'] sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels']
} }
] ],
hiddenKeys: ['name', 'attachedTo'],
options: {
lookup: {
_id: {
related: [tracker.class.Issue, 'relations._id']
}
}
}
}, },
recruit.viewlet.TableApplicant recruit.viewlet.TableApplicant
) )
@ -413,18 +448,40 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.Table, descriptor: view.viewlet.Table,
config: [ config: [
'', '',
'attachedTo', {
key: 'attachedTo',
presenter: contact.component.PersonRefPresenter,
label: recruit.string.Talent,
sortingKey: 'attachedTo',
props: {
_class: recruit.mixin.Candidate
}
},
'assignee', 'assignee',
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Issues
},
'state', 'state',
'comments', 'comments',
'attachments', 'attachments',
'modifiedOn', 'modifiedOn',
'$lookup.space.company',
{ {
key: '$lookup.attachedTo.$lookup.channels', key: '$lookup.attachedTo.$lookup.channels',
label: contact.string.ContactInfo,
sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels'] sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels']
} }
], ],
hiddenKeys: ['name'] options: {
lookup: {
_id: {
related: [tracker.class.Issue, 'relations._id']
}
}
},
hiddenKeys: ['name', 'attachedTo']
}, },
recruit.viewlet.ApplicantTable recruit.viewlet.ApplicantTable
) )
@ -452,7 +509,7 @@ export function createModel (builder: Builder): void {
], ],
assignee: contact.class.Employee, assignee: contact.class.Employee,
_id: { _id: {
todoItems: task.class.TodoItem related: [tracker.class.Issue, 'relations._id']
} }
} }
@ -672,7 +729,7 @@ export function createModel (builder: Builder): void {
}) })
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ClassFilters, { builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ClassFilters, {
filters: ['attachedTo', 'assignee', 'state', 'doneState', 'modifiedOn'] filters: ['attachedTo', 'space', 'assignee', 'state', 'doneState', 'modifiedOn']
}) })
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ClassFilters, { builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ClassFilters, {
@ -917,6 +974,25 @@ export function createModel (builder: Builder): void {
group: 'create' group: 'create'
} }
}) })
builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ObjectEditorFooter, {
editor: tracker.component.RelatedIssuesSection,
props: {
label: recruit.string.RelatedIssues
}
})
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectEditorFooter, {
editor: tracker.component.RelatedIssuesSection,
props: {
label: recruit.string.RelatedIssues
}
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectEditorFooter, {
editor: tracker.component.RelatedIssuesSection,
props: {
label: recruit.string.RelatedIssues
}
})
} }
export { recruitOperation } from './migration' export { recruitOperation } from './migration'

View File

@ -134,8 +134,8 @@ export class TTask extends TAttachedDoc implements Task {
declare rank: string declare rank: string
@Prop(Collection(task.class.TodoItem), task.string.Todos) // @Prop(Collection(task.class.TodoItem), task.string.Todos)
todoItems!: number // todoItems!: number
@Prop(Collection(tags.class.TagReference, task.string.TaskLabels), task.string.TaskLabels) @Prop(Collection(tags.class.TagReference, task.string.TaskLabels), task.string.TaskLabels)
labels!: number labels!: number

View File

@ -46,6 +46,7 @@ import type {
ListItemPresenter, ListItemPresenter,
ObjectEditor, ObjectEditor,
ObjectEditorHeader, ObjectEditorHeader,
ObjectEditorFooter,
ObjectFactory, ObjectFactory,
ObjectPresenter, ObjectPresenter,
ObjectTitle, ObjectTitle,
@ -173,6 +174,11 @@ export class TObjectEditorHeader extends TClass implements ObjectEditorHeader {
editor!: AnyComponent editor!: AnyComponent
} }
@Mixin(view.mixin.ObjectEditorFooter, core.class.Class)
export class TObjectEditorFooter extends TClass implements ObjectEditorFooter {
editor!: AnyComponent
}
@Mixin(view.mixin.SpaceHeader, core.class.Class) @Mixin(view.mixin.SpaceHeader, core.class.Class)
export class TSpaceHeader extends TClass implements SpaceHeader { export class TSpaceHeader extends TClass implements SpaceHeader {
header!: AnyComponent header!: AnyComponent
@ -325,6 +331,7 @@ export function createModel (builder: Builder): void {
TObjectFactory, TObjectFactory,
TObjectTitle, TObjectTitle,
TObjectEditorHeader, TObjectEditorHeader,
TObjectEditorFooter,
TSpaceHeader, TSpaceHeader,
TSpaceName, TSpaceName,
TIgnoreActions, TIgnoreActions,

View File

@ -71,7 +71,14 @@
{#if icon}<div class="wrapped-icon"><Icon {icon} size={'medium'} /></div>{/if} {#if icon}<div class="wrapped-icon"><Icon {icon} size={'medium'} /></div>{/if}
<div class="title-wrapper"> <div class="title-wrapper">
{#if title}<span class="wrapped-title">{title}</span>{/if} {#if title}<span class="wrapped-title">{title}</span>{/if}
{#if subtitle}<span class="wrapped-subtitle">{subtitle}</span>{/if} {#if subtitle || $$slots.subtitle}
<span class="wrapped-subtitle">
{#if subtitle}
{subtitle}
{/if}
<slot name="subtitle" />
</span>
{/if}
</div> </div>
</div> </div>
{/if} {/if}

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Contact, Employee, formatName } from '@hcengineering/contact' import contact, { Contact, Employee, formatName } from '@hcengineering/contact'
import { Class, DocumentQuery, FindOptions, Ref } from '@hcengineering/core' import { Class, DocumentQuery, FindOptions, Hierarchy, Ref } from '@hcengineering/core'
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform' import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import { import {
ActionIcon, ActionIcon,
@ -166,7 +166,7 @@
size={'small'} size={'small'}
action={() => { action={() => {
if (selected) { if (selected) {
showPanel(view.component.EditDoc, selected._id, selected._class, 'content') showPanel(view.component.EditDoc, selected._id, Hierarchy.mixinOrClass(selected), 'content')
} }
}} }}
/> />

View File

@ -15,7 +15,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Contact, formatName } from '@hcengineering/contact' import contact, { Contact, formatName } from '@hcengineering/contact'
import type { Class, DocumentQuery, FindOptions, Ref } from '@hcengineering/core' import { Class, DocumentQuery, FindOptions, Hierarchy, Ref } from '@hcengineering/core'
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform' import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import { import {
ActionIcon, ActionIcon,
@ -180,7 +180,7 @@
size={'small'} size={'small'}
action={() => { action={() => {
if (selected) { if (selected) {
showPanel(view.component.EditDoc, selected._id, selected._class, 'content') showPanel(view.component.EditDoc, selected._id, Hierarchy.mixinOrClass(selected), 'content')
} }
}} }}
/> />

View File

@ -644,6 +644,7 @@ a.no-line {
.pointer-events-none { pointer-events: none; } .pointer-events-none { pointer-events: none; }
.select-text { user-select: text; } .select-text { user-select: text; }
.select-text-i { user-select: text !important; }
/* Text */ /* Text */

View File

@ -15,6 +15,7 @@
<script lang="ts"> <script lang="ts">
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import contact, { Channel, Contact, formatName } from '@hcengineering/contact' import contact, { Channel, Contact, formatName } from '@hcengineering/contact'
import { Hierarchy } from '@hcengineering/core'
import { Avatar, createQuery } from '@hcengineering/presentation' import { Avatar, createQuery } from '@hcengineering/presentation'
import { Component, Label, showPanel } from '@hcengineering/ui' import { Component, Label, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
@ -42,11 +43,12 @@
<Avatar avatar={object.avatar} size={'large'} icon={contact.icon.Company} /> <Avatar avatar={object.avatar} size={'large'} icon={contact.icon.Company} />
</div> </div>
{#if object} {#if object}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
class="name lines-limit-2" class="name lines-limit-2"
class:over-underline={!disabled} class:over-underline={!disabled}
on:click={() => { on:click={() => {
if (!disabled) showPanel(view.component.EditDoc, object._id, object._class, 'content') if (!disabled) showPanel(view.component.EditDoc, object._id, Hierarchy.mixinOrClass(object), 'content')
}} }}
> >
{formatName(object.name)} {formatName(object.name)}

View File

@ -14,13 +14,14 @@
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Person } from '@hcengineering/contact' import contact, { Person } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core' import { Class, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import { PersonLabelTooltip } from '..' import { PersonLabelTooltip } from '..'
import PersonPresenter from './PersonPresenter.svelte' import PersonPresenter from './PersonPresenter.svelte'
export let value: Ref<Person> | null | undefined export let value: Ref<Person> | null | undefined
export let _class: Ref<Class<Person>> = contact.class.Person
export let inline = false export let inline = false
export let enlargedText = false export let enlargedText = false
export let isInteractive = true export let isInteractive = true
@ -34,7 +35,7 @@
let person: Person | undefined let person: Person | undefined
const query = createQuery() const query = createQuery()
$: value && query.query(contact.class.Person, { _id: value }, (res) => ([person] = res), { limit: 1 }) $: value && query.query(_class, { _id: value }, (res) => ([person] = res), { limit: 1 })
function getValue (person: Person | undefined, value: Ref<Person> | null | undefined): Person | null | undefined { function getValue (person: Person | undefined, value: Ref<Person> | null | undefined): Person | null | undefined {
if (value === undefined || value === null) { if (value === undefined || value === null) {

View File

@ -67,7 +67,8 @@ export {
EmployeeBrowser, EmployeeBrowser,
MemberPresenter, MemberPresenter,
EmployeeEditor, EmployeeEditor,
EmployeeAccountRefPresenter EmployeeAccountRefPresenter,
EditPerson
} }
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({ const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({

View File

@ -12,7 +12,7 @@
"HaveWorkspace": "Уже есть рабочее пространство?", "HaveWorkspace": "Уже есть рабочее пространство?",
"LastName": "Фамилия", "LastName": "Фамилия",
"FirstName": "Имя", "FirstName": "Имя",
"Join": "Присоедениться", "Join": "Присоединиться",
"Email": "Email", "Email": "Email",
"Password": "Пароль", "Password": "Пароль",
"Workspace": "Рабочее пространство", "Workspace": "Рабочее пространство",

View File

@ -17,6 +17,7 @@
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import contact, { Channel, formatName, Person } from '@hcengineering/contact' import contact, { Channel, formatName, Person } from '@hcengineering/contact'
import { ChannelsEditor } from '@hcengineering/contact-resources' import { ChannelsEditor } from '@hcengineering/contact-resources'
import { Hierarchy } from '@hcengineering/core'
import { Avatar, createQuery, getClient } from '@hcengineering/presentation' import { Avatar, createQuery, getClient } from '@hcengineering/presentation'
import { Component, Label, showPanel } from '@hcengineering/ui' import { Component, Label, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
@ -47,12 +48,13 @@
<div class="label uppercase"><Label label={recruit.string.Talent} /></div> <div class="label uppercase"><Label label={recruit.string.Talent} /></div>
<Avatar avatar={candidate?.avatar} size={'large'} /> <Avatar avatar={candidate?.avatar} size={'large'} />
{#if candidate} {#if candidate}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
class="name lines-limit-2" class="name lines-limit-2"
class:over-underline={!disabled} class:over-underline={!disabled}
on:click={() => { on:click={() => {
if (!disabled && candidate) { if (!disabled && candidate) {
showPanel(view.component.EditDoc, candidate._id, candidate._class, 'content') showPanel(view.component.EditDoc, candidate._id, Hierarchy.mixinOrClass(candidate), 'content')
} }
}} }}
> >

View File

@ -14,16 +14,16 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import type { Candidate, Applicant, Vacancy } from '@hcengineering/recruit' import type { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import { Scroller } from '@hcengineering/ui' import { Scroller } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import CandidateCard from './CandidateCard.svelte' import CandidateCard from './CandidateCard.svelte'
import VacancyCard from './VacancyCard.svelte'
import ExpandRightDouble from './icons/ExpandRightDouble.svelte' import ExpandRightDouble from './icons/ExpandRightDouble.svelte'
import VacancyCard from './VacancyCard.svelte'
import recruit from '../plugin'
import { Ref } from '@hcengineering/core' import { Ref } from '@hcengineering/core'
import recruit from '../plugin'
import Reviews from './review/Reviews.svelte' import Reviews from './review/Reviews.svelte'
export let object: Applicant export let object: Applicant

View File

@ -21,8 +21,7 @@
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Vacancy } from '@hcengineering/recruit' import { Vacancy } from '@hcengineering/recruit'
import { FullDescriptionBox } from '@hcengineering/text-editor' import { FullDescriptionBox } from '@hcengineering/text-editor'
import tracker from '@hcengineering/tracker' import { Button, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
import { Button, Component, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources' import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import recruit from '../plugin' import recruit from '../plugin'
@ -65,7 +64,6 @@
<Panel <Panel
icon={clazz.icon} icon={clazz.icon}
title={object.name} title={object.name}
subtitle={object.description}
isHeader={true} isHeader={true}
isAside={true} isAside={true}
{object} {object}
@ -73,6 +71,11 @@
dispatch('close') dispatch('close')
}} }}
> >
<svelte:fragment slot="subtitle">
<a href={object.description} target="_blank" rel="noreferrer noopener">
{object.description}
</a>
</svelte:fragment>
<svelte:fragment slot="attributes" let:direction={dir}> <svelte:fragment slot="attributes" let:direction={dir}>
{#if dir === 'column'} {#if dir === 'column'}
<div class="ac-subtitle"> <div class="ac-subtitle">
@ -129,9 +132,6 @@
space={object.space} space={object.space}
attachments={object.attachments ?? 0} attachments={object.attachments ?? 0}
/> />
<!-- <MembersBox label={recruit.string.Members} space={object} /> -->
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
</Grid> </Grid>
</Panel> </Panel>
{/if} {/if}

View File

@ -16,13 +16,13 @@
import { AttachmentsPresenter } from '@hcengineering/attachment-resources' import { AttachmentsPresenter } from '@hcengineering/attachment-resources'
import { CommentsPresenter } from '@hcengineering/chunter-resources' import { CommentsPresenter } from '@hcengineering/chunter-resources'
import contact, { formatName } from '@hcengineering/contact' import contact, { formatName } from '@hcengineering/contact'
import type { WithLookup } from '@hcengineering/core' import { Hierarchy, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { Avatar } from '@hcengineering/presentation' import { Avatar } from '@hcengineering/presentation'
import type { Applicant, Candidate } from '@hcengineering/recruit' import type { Applicant, Candidate } from '@hcengineering/recruit'
import task, { TodoItem } from '@hcengineering/task'
import { AssigneePresenter } from '@hcengineering/task-resources' import { AssigneePresenter } from '@hcengineering/task-resources'
import { Component, showPanel, tooltip } from '@hcengineering/ui' import tracker from '@hcengineering/tracker'
import { Component, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import ApplicationPresenter from './ApplicationPresenter.svelte' import ApplicationPresenter from './ApplicationPresenter.svelte'
@ -30,15 +30,13 @@
export let dragged: boolean export let dragged: boolean
function showCandidate () { function showCandidate () {
showPanel(view.component.EditDoc, object._id, object._class, 'content') showPanel(view.component.EditDoc, object._id, Hierarchy.mixinOrClass(object), 'content')
} }
$: todoItems = (object.$lookup?.todoItems as TodoItem[]) ?? []
$: doneTasks = todoItems.filter((it) => it.done)
$: channels = (object.$lookup?.attachedTo as WithLookup<Candidate>)?.$lookup?.channels $: channels = (object.$lookup?.attachedTo as WithLookup<Candidate>)?.$lookup?.channels
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-col pt-2 pb-2 pr-4 pl-4 cursor-pointer" on:click={showCandidate}> <div class="flex-col pt-2 pb-2 pr-4 pl-4 cursor-pointer" on:click={showCandidate}>
<div class="flex-between mb-3"> <div class="flex-between mb-3">
<div class="flex-row-center"> <div class="flex-row-center">
@ -72,18 +70,7 @@
<div class="flex-row-center"> <div class="flex-row-center">
<div class="sm-tool-icon step-lr75"> <div class="sm-tool-icon step-lr75">
<ApplicationPresenter value={object} /> <ApplicationPresenter value={object} />
{#if todoItems.length > 0} <Component is={tracker.component.RelatedIssueSelector} props={{ object }} />
<div
class="ml-2"
use:tooltip={{
label: task.string.TodoItems,
component: task.component.TodoItemsPopup,
props: { value: object }
}}
>
({doneTasks?.length}/{todoItems.length})
</div>
{/if}
</div> </div>
{#if (object.attachments ?? 0) > 0} {#if (object.attachments ?? 0) > 0}
<div class="step-lr75"> <div class="step-lr75">

View File

@ -16,6 +16,7 @@
<script lang="ts"> <script lang="ts">
import calendar from '@hcengineering/calendar' import calendar from '@hcengineering/calendar'
import contact, { Contact } from '@hcengineering/contact' import contact, { Contact } from '@hcengineering/contact'
import { Hierarchy } from '@hcengineering/core'
import { getClient, UserBox } from '@hcengineering/presentation' import { getClient, UserBox } from '@hcengineering/presentation'
import type { Review } from '@hcengineering/recruit' import type { Review } from '@hcengineering/recruit'
import { FullDescriptionBox } from '@hcengineering/text-editor' import { FullDescriptionBox } from '@hcengineering/text-editor'
@ -70,7 +71,7 @@
class="clear-mins" class="clear-mins"
on:click={() => { on:click={() => {
if (candidate !== undefined) { if (candidate !== undefined) {
showPanel(view.component.EditDoc, candidate._id, candidate._class, 'content') showPanel(view.component.EditDoc, candidate._id, Hierarchy.mixinOrClass(candidate), 'content')
} }
}} }}
> >

View File

@ -185,7 +185,7 @@
<UpDownNavigator element={issue} /> <UpDownNavigator element={issue} />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<span class="fs-title"> <span class="fs-title select-text-i">
{#if issueId}{issueId}{/if} {#if issueId}{issueId}{/if}
</span> </span>
</svelte:fragment> </svelte:fragment>
@ -265,7 +265,7 @@
</div> </div>
{/if} {/if}
<span slot="actions-label"> <span slot="actions-label" class="select-text">
{#if issueId}{issueId}{/if} {#if issueId}{issueId}{/if}
</span> </span>
<svelte:fragment slot="actions"> <svelte:fragment slot="actions">

View File

@ -0,0 +1,153 @@
<!--
// 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, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import {
Button,
ButtonKind,
ButtonSize,
closeTooltip,
getPlatformColor,
ProgressCircle,
SelectPopup,
showPanel,
showPopup
} from '@hcengineering/ui'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import { subIssueListProvider } from '../../../utils'
export let object: WithLookup<Doc & { related: number }> | undefined
export let value: WithLookup<Doc & { related: number }> | undefined
export let currentTeam: Team | undefined
export let kind: ButtonKind = 'link-bordered'
export let size: ButtonSize = 'inline'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = 'min-contet'
let btn: HTMLElement
let subIssues: Issue[] = []
let countComplate: number = 0
const query = createQuery()
const statusesQuery = createQuery()
$: _object = object ?? value
$: _object && update(_object)
function update (value: WithLookup<Doc & { related: number }>): void {
if (value.$lookup?.related !== undefined) {
query.unsubscribe()
subIssues = value.$lookup.related as Issue[]
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
} else {
query.query(
tracker.class.Issue,
{ 'relations._id': value._id, 'relations._class': value._class },
(res) => (subIssues = res),
{
sort: { rank: SortingOrder.Ascending }
}
)
}
statusesQuery.query(tracker.class.IssueStatus, {}, (res) => (statuses = res), {
lookup: { category: tracker.class.IssueStatusCategory }
})
}
let statuses: WithLookup<IssueStatus>[] = []
$: if (statuses && subIssues) {
const doneStatuses = statuses.filter((s) => s.category === tracker.issueStatusCategory.Completed).map((p) => p._id)
countComplate = subIssues.filter((si) => doneStatuses.includes(si.status)).length
}
$: hasSubIssues = (subIssues?.length ?? 0) > 0
function getIssueStatusIcon (issue: Issue, statuses: WithLookup<IssueStatus>[] | undefined) {
const status = statuses?.find((s) => issue.status === s._id)
const category = status?.$lookup?.category
const color = status?.color ?? category?.color
return {
...(category?.icon !== undefined ? { icon: category.icon } : {}),
...(color !== undefined ? { iconColor: getPlatformColor(color) } : {})
}
}
function openIssue (target: Ref<Issue>) {
subIssueListProvider(subIssues, target)
showPanel(tracker.component.EditIssue, target, tracker.class.Issue, 'content')
}
function showSubIssues () {
if (subIssues) {
closeTooltip()
showPopup(
SelectPopup,
{
value: subIssues.map((iss) => {
const text = currentTeam ? `${getIssueId(currentTeam, iss)} ${iss.title}` : iss.title
return { id: iss._id, text, isSelected: false, ...getIssueStatusIcon(iss, statuses) }
}),
width: 'large'
},
{
getBoundingClientRect: () => {
const rect = btn.getBoundingClientRect()
const offsetX = 0
const offsetY = 0
return DOMRect.fromRect({ width: 1, height: 1, x: rect.left + offsetX, y: rect.bottom + offsetY })
}
},
(selectedIssue) => {
selectedIssue !== undefined && openIssue(selectedIssue)
}
)
}
}
</script>
{#if hasSubIssues}
<div class="flex-center flex-no-shrink" bind:this={btn}>
<Button
{width}
{kind}
{size}
{justify}
on:click={(ev) => {
ev.stopPropagation()
if (subIssues) showSubIssues()
}}
>
<svelte:fragment slot="content">
{#if subIssues}
<div class="flex-row-center content-color text-sm pointer-events-none">
<div class="mr-1">
<ProgressCircle bind:value={countComplate} bind:max={subIssues.length} size={'inline'} primary />
</div>
{countComplate}/{subIssues.length}
</div>
{/if}
</svelte:fragment>
</Button>
</div>
{/if}

View File

@ -61,6 +61,7 @@ import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.sv
import Views from './components/views/Views.svelte' import Views from './components/views/Views.svelte'
import Statuses from './components/workflow/Statuses.svelte' import Statuses from './components/workflow/Statuses.svelte'
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte' import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.svelte'
import { import {
getIssueId, getIssueId,
getIssueTitle, getIssueTitle,
@ -280,7 +281,8 @@ export default async (): Promise<Resources> => ({
TeamPresenter, TeamPresenter,
IssueStatistics, IssueStatistics,
StatusRefPresenter, StatusRefPresenter,
RelatedIssuesSection RelatedIssuesSection,
RelatedIssueSelector
}, },
completion: { completion: {
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) => IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>

View File

@ -400,6 +400,7 @@ export default plugin(trackerId, {
TrackerApp: '' as AnyComponent, TrackerApp: '' as AnyComponent,
RelatedIssues: '' as AnyComponent, RelatedIssues: '' as AnyComponent,
RelatedIssuesSection: '' as AnyComponent, RelatedIssuesSection: '' as AnyComponent,
RelatedIssueSelector: '' as AnyComponent,
RelatedIssueTemplates: '' as AnyComponent, RelatedIssueTemplates: '' as AnyComponent,
EditIssue: '' as AnyComponent, EditIssue: '' as AnyComponent,
CreateIssue: '' as AnyComponent, CreateIssue: '' as AnyComponent,

View File

@ -35,8 +35,8 @@
import { categorizeFields, getCollectionCounter, getFiltredKeys } from '../utils' import { categorizeFields, getCollectionCounter, getFiltredKeys } from '../utils'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import DocAttributeBar from './DocAttributeBar.svelte' import DocAttributeBar from './DocAttributeBar.svelte'
import UpDownNavigator from './UpDownNavigator.svelte'
import IconMixin from './icons/Mixin.svelte' import IconMixin from './icons/Mixin.svelte'
import UpDownNavigator from './UpDownNavigator.svelte'
export let _id: Ref<Doc> export let _id: Ref<Doc>
export let _class: Ref<Class<Doc>> export let _class: Ref<Class<Doc>>
@ -147,19 +147,32 @@
pinned?: boolean pinned?: boolean
} }
async function getEditor (_class: Ref<Class<Doc>>): Promise<MixinEditor> { function getEditor (_class: Ref<Class<Doc>>): MixinEditor {
const clazz = hierarchy.getClass(_class) const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor) const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
if (editorMixin?.editor == null && clazz.extends != null) return getEditor(clazz.extends) if (editorMixin?.editor == null && clazz.extends != null) return getEditor(clazz.extends)
return { editor: editorMixin.editor, pinned: editorMixin?.pinned } return { editor: editorMixin.editor, pinned: editorMixin?.pinned }
} }
function getEditorFooter (_class: Ref<Class<Doc>>): { footer: AnyComponent; props?: Record<string, any> } | undefined {
const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditorFooter)
if (editorMixin?.editor == null && clazz.extends != null) return getEditorFooter(clazz.extends)
if (editorMixin.editor) {
return { footer: editorMixin.editor, props: editorMixin?.props }
}
return undefined
}
let mainEditor: MixinEditor | undefined let mainEditor: MixinEditor | undefined
$: editorFooter = getEditorFooter(_class)
$: getEditorOrDefault(realObjectClass, showAllMixins, _id) $: getEditorOrDefault(realObjectClass, showAllMixins, _id)
async function getEditorOrDefault (_class: Ref<Class<Doc>>, showAllMixins: boolean, _id: Ref<Doc>): Promise<void> { function getEditorOrDefault (_class: Ref<Class<Doc>>, showAllMixins: boolean, _id: Ref<Doc>): void {
parentClass = getParentClass(_class) parentClass = getParentClass(_class)
mainEditor = await getEditor(_class) mainEditor = getEditor(_class)
updateKeys(showAllMixins) updateKeys(showAllMixins)
} }
@ -370,5 +383,8 @@
</div> </div>
{/if} {/if}
{/each} {/each}
{#if editorFooter}
<Component is={editorFooter.footer} props={{ object, _class, ...editorFooter.props }} />
{/if}
</Panel> </Panel>
{/if} {/if}

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core' import { Class, Doc, DocumentQuery, FindOptions, Hierarchy, Ref } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform' import { Asset, IntlString } from '@hcengineering/platform'
import presentation, { getClient, ObjectCreate } from '@hcengineering/presentation' import presentation, { getClient, ObjectCreate } from '@hcengineering/presentation'
import { import {
@ -151,7 +151,7 @@
size={'small'} size={'small'}
action={() => { action={() => {
if (selected) { if (selected) {
showPanel(view.component.EditDoc, selected._id, selected._class, 'content') showPanel(view.component.EditDoc, selected._id, Hierarchy.mixinOrClass(selected), 'content')
} }
}} }}
/> />

View File

@ -57,7 +57,7 @@
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
$: lookup = options?.lookup ?? buildConfigLookup(hierarchy, _class, config) $: lookup = buildConfigLookup(hierarchy, _class, config, options?.lookup)
let _sortKey = prefferedSorting let _sortKey = prefferedSorting
$: if (!userSorting) { $: if (!userSorting) {
@ -109,7 +109,7 @@
dispatch('content', objects) dispatch('content', objects)
loading = loading === 1 ? 0 : -1 loading = loading === 1 ? 0 : -1
}, },
{ sort, limit: 200, lookup, ...options } { sort, limit: 200, ...options, lookup }
) )
if (update && ++loading > 0) { if (update && ++loading > 0) {
objects = [] objects = []

View File

@ -15,7 +15,7 @@
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config) const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config, viewlet.options?.lookup)
const groupBy = config.groupBy const groupBy = config.groupBy
.map((p) => { .map((p) => {

View File

@ -68,7 +68,7 @@
} }
function getBaseConfig (viewlet: Viewlet): AttributeConfig[] { function getBaseConfig (viewlet: Viewlet): AttributeConfig[] {
const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config) const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config, viewlet.options?.lookup)
const result: AttributeConfig[] = [] const result: AttributeConfig[] = []
for (const param of viewlet.config) { for (const param of viewlet.config) {
if (typeof param === 'string') { if (typeof param === 'string') {

View File

@ -48,8 +48,8 @@
$: orderBy = viewOptions.orderBy $: orderBy = viewOptions.orderBy
const docsQuery = createQuery() const docsQuery = createQuery()
$: lookup = options?.lookup ?? buildConfigLookup(client.getHierarchy(), _class, config) $: lookup = buildConfigLookup(client.getHierarchy(), _class, config, options?.lookup)
$: resultOptions = { lookup, ...options, sort: { [orderBy[0]]: orderBy[1] } } $: resultOptions = { ...options, lookup, sort: { [orderBy[0]]: orderBy[1] } }
let resultQuery: DocumentQuery<Doc> = query let resultQuery: DocumentQuery<Doc> = query
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => { $: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => {

View File

@ -25,7 +25,9 @@ import core, {
Obj, Obj,
Ref, Ref,
RefTo, RefTo,
TxOperations TxOperations,
ReverseLookup,
ReverseLookups
} from '@hcengineering/core' } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
@ -234,7 +236,8 @@ function getKeyLookup<T extends Doc> (
export function buildConfigLookup<T extends Doc> ( export function buildConfigLookup<T extends Doc> (
hierarchy: Hierarchy, hierarchy: Hierarchy,
_class: Ref<Class<T>>, _class: Ref<Class<T>>,
config: Array<BuildModelKey | string> config: Array<BuildModelKey | string>,
existingLookup?: Lookup<T>
): Lookup<T> { ): Lookup<T> {
let res: Lookup<T> = {} let res: Lookup<T> = {}
for (const key of config) { for (const key of config) {
@ -244,6 +247,14 @@ export function buildConfigLookup<T extends Doc> (
res = getKeyLookup(hierarchy, _class, key.key, res) res = getKeyLookup(hierarchy, _class, key.key, res)
} }
} }
if (existingLookup !== undefined) {
// Let's merg
const _id: ReverseLookup = {
...((existingLookup as ReverseLookups)._id ?? {}),
...((res as ReverseLookups)._id ?? {})
}
res = { ...existingLookup, ...res, _id }
}
return res return res
} }

View File

@ -168,6 +168,14 @@ export interface ObjectEditor extends Class<Doc> {
pinned?: boolean pinned?: boolean
} }
/**
* @public
*/
export interface ObjectEditorFooter extends Class<Doc> {
editor: AnyComponent
props?: Record<string, any>
}
/** /**
* @public * @public
*/ */
@ -523,6 +531,7 @@ const view = plugin(viewId, {
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>, ObjectEditor: '' as Ref<Mixin<ObjectEditor>>,
ObjectPresenter: '' as Ref<Mixin<ObjectPresenter>>, ObjectPresenter: '' as Ref<Mixin<ObjectPresenter>>,
ObjectEditorHeader: '' as Ref<Mixin<ObjectEditorHeader>>, ObjectEditorHeader: '' as Ref<Mixin<ObjectEditorHeader>>,
ObjectEditorFooter: '' as Ref<Mixin<ObjectEditorFooter>>,
ObjectValidator: '' as Ref<Mixin<ObjectValidator>>, ObjectValidator: '' as Ref<Mixin<ObjectValidator>>,
ObjectFactory: '' as Ref<Mixin<ObjectFactory>>, ObjectFactory: '' as Ref<Mixin<ObjectFactory>>,
ObjectTitle: '' as Ref<Mixin<ObjectTitle>>, ObjectTitle: '' as Ref<Mixin<ObjectTitle>>,

View File

@ -13,7 +13,7 @@
"View": "Посмотреть", "View": "Посмотреть",
"Leave": "Покинуть", "Leave": "Покинуть",
"Joined": "Вы присоеденились", "Joined": "Вы присоеденились",
"Join": "Присоедениться", "Join": "Присоединиться",
"BrowseSpaces": "Обзор пространств", "BrowseSpaces": "Обзор пространств",
"AccountDisabled": "Аккаунт отключен", "AccountDisabled": "Аккаунт отключен",
"AccountDisabledDescr": "Пожалуйста свяжитесь с администратором", "AccountDisabledDescr": "Пожалуйста свяжитесь с администратором",

View File

@ -267,7 +267,7 @@ class TServerStorage implements ServerStorage {
): Promise<FindResult<T>> { ): Promise<FindResult<T>> {
return await ctx.with('find-all', {}, (ctx) => { return await ctx.with('find-all', {}, (ctx) => {
const domain = this.hierarchy.getDomain(clazz) const domain = this.hierarchy.getDomain(clazz)
if (query.$search !== undefined) { if (query?.$search !== undefined) {
return ctx.with('full-text-find-all', {}, (ctx) => this.fulltext.findAll(ctx, clazz, query, options)) return ctx.with('full-text-find-all', {}, (ctx) => this.fulltext.findAll(ctx, clazz, query, options))
} }
return ctx.with('db-find-all', { _class: clazz, domain }, () => return ctx.with('db-find-all', { _class: clazz, domain }, () =>

View File

@ -181,6 +181,7 @@ describe('mongo operations', () => {
}) })
it('check add', async () => { it('check add', async () => {
jest.setTimeout(50000)
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, { await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: `my-task-${i}`, name: `my-task-${i}`,