mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
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:
parent
317e705b39
commit
d1fb81582d
@ -44,6 +44,7 @@
|
||||
"@hcengineering/setting": "^0.6.2",
|
||||
"@hcengineering/model-task": "^0.6.0",
|
||||
"@hcengineering/workbench": "^0.6.2",
|
||||
"@hcengineering/model-tracker": "^0.6.0",
|
||||
"@hcengineering/model-presentation": "^0.6.0",
|
||||
"@hcengineering/model-calendar": "^0.6.0",
|
||||
"@hcengineering/model-tags": "^0.6.0",
|
||||
|
@ -39,6 +39,7 @@ import core, { TAttachedDoc, TSpace } from '@hcengineering/model-core'
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
import tags from '@hcengineering/model-tags'
|
||||
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 workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench'
|
||||
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||
@ -331,6 +332,11 @@ export function createModel (builder: Builder): void {
|
||||
'city',
|
||||
'applications',
|
||||
'attachments',
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
label: tracker.string.Relations
|
||||
},
|
||||
'comments',
|
||||
{
|
||||
// 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']
|
||||
}
|
||||
],
|
||||
hiddenKeys: ['name']
|
||||
hiddenKeys: ['name'],
|
||||
options: {
|
||||
lookup: {
|
||||
_id: {
|
||||
related: [tracker.class.Issue, 'relations._id']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
recruit.viewlet.TableCandidate
|
||||
)
|
||||
@ -390,8 +403,21 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: task.viewlet.StatusTable,
|
||||
config: [
|
||||
'',
|
||||
'attachedTo',
|
||||
{
|
||||
key: 'attachedTo',
|
||||
presenter: contact.component.PersonRefPresenter,
|
||||
sortingKey: 'attachedTo',
|
||||
label: recruit.string.Talent,
|
||||
props: {
|
||||
_class: recruit.mixin.Candidate
|
||||
}
|
||||
},
|
||||
'assignee',
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
label: tracker.string.Issues
|
||||
},
|
||||
'state',
|
||||
'doneState',
|
||||
'attachments',
|
||||
@ -399,9 +425,18 @@ export function createModel (builder: Builder): void {
|
||||
'modifiedOn',
|
||||
{
|
||||
key: '$lookup.attachedTo.$lookup.channels',
|
||||
label: contact.string.ContactInfo,
|
||||
sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels']
|
||||
}
|
||||
]
|
||||
],
|
||||
hiddenKeys: ['name', 'attachedTo'],
|
||||
options: {
|
||||
lookup: {
|
||||
_id: {
|
||||
related: [tracker.class.Issue, 'relations._id']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
recruit.viewlet.TableApplicant
|
||||
)
|
||||
@ -413,18 +448,40 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: view.viewlet.Table,
|
||||
config: [
|
||||
'',
|
||||
'attachedTo',
|
||||
{
|
||||
key: 'attachedTo',
|
||||
presenter: contact.component.PersonRefPresenter,
|
||||
label: recruit.string.Talent,
|
||||
sortingKey: 'attachedTo',
|
||||
props: {
|
||||
_class: recruit.mixin.Candidate
|
||||
}
|
||||
},
|
||||
'assignee',
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.RelatedIssueSelector,
|
||||
label: tracker.string.Issues
|
||||
},
|
||||
'state',
|
||||
'comments',
|
||||
'attachments',
|
||||
'modifiedOn',
|
||||
'$lookup.space.company',
|
||||
{
|
||||
key: '$lookup.attachedTo.$lookup.channels',
|
||||
label: contact.string.ContactInfo,
|
||||
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
|
||||
)
|
||||
@ -452,7 +509,7 @@ export function createModel (builder: Builder): void {
|
||||
],
|
||||
assignee: contact.class.Employee,
|
||||
_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, {
|
||||
filters: ['attachedTo', 'assignee', 'state', 'doneState', 'modifiedOn']
|
||||
filters: ['attachedTo', 'space', 'assignee', 'state', 'doneState', 'modifiedOn']
|
||||
})
|
||||
|
||||
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ClassFilters, {
|
||||
@ -917,6 +974,25 @@ export function createModel (builder: Builder): void {
|
||||
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'
|
||||
|
@ -134,8 +134,8 @@ export class TTask extends TAttachedDoc implements Task {
|
||||
|
||||
declare rank: string
|
||||
|
||||
@Prop(Collection(task.class.TodoItem), task.string.Todos)
|
||||
todoItems!: number
|
||||
// @Prop(Collection(task.class.TodoItem), task.string.Todos)
|
||||
// todoItems!: number
|
||||
|
||||
@Prop(Collection(tags.class.TagReference, task.string.TaskLabels), task.string.TaskLabels)
|
||||
labels!: number
|
||||
|
@ -46,6 +46,7 @@ import type {
|
||||
ListItemPresenter,
|
||||
ObjectEditor,
|
||||
ObjectEditorHeader,
|
||||
ObjectEditorFooter,
|
||||
ObjectFactory,
|
||||
ObjectPresenter,
|
||||
ObjectTitle,
|
||||
@ -173,6 +174,11 @@ export class TObjectEditorHeader extends TClass implements ObjectEditorHeader {
|
||||
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)
|
||||
export class TSpaceHeader extends TClass implements SpaceHeader {
|
||||
header!: AnyComponent
|
||||
@ -325,6 +331,7 @@ export function createModel (builder: Builder): void {
|
||||
TObjectFactory,
|
||||
TObjectTitle,
|
||||
TObjectEditorHeader,
|
||||
TObjectEditorFooter,
|
||||
TSpaceHeader,
|
||||
TSpaceName,
|
||||
TIgnoreActions,
|
||||
|
@ -71,7 +71,14 @@
|
||||
{#if icon}<div class="wrapped-icon"><Icon {icon} size={'medium'} /></div>{/if}
|
||||
<div class="title-wrapper">
|
||||
{#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>
|
||||
{/if}
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 {
|
||||
ActionIcon,
|
||||
@ -166,7 +166,7 @@
|
||||
size={'small'}
|
||||
action={() => {
|
||||
if (selected) {
|
||||
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||
showPanel(view.component.EditDoc, selected._id, Hierarchy.mixinOrClass(selected), 'content')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -15,7 +15,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 {
|
||||
ActionIcon,
|
||||
@ -180,7 +180,7 @@
|
||||
size={'small'}
|
||||
action={() => {
|
||||
if (selected) {
|
||||
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||
showPanel(view.component.EditDoc, selected._id, Hierarchy.mixinOrClass(selected), 'content')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -644,6 +644,7 @@ a.no-line {
|
||||
|
||||
.pointer-events-none { pointer-events: none; }
|
||||
.select-text { user-select: text; }
|
||||
.select-text-i { user-select: text !important; }
|
||||
|
||||
/* Text */
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
<script lang="ts">
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import contact, { Channel, Contact, formatName } from '@hcengineering/contact'
|
||||
import { Hierarchy } from '@hcengineering/core'
|
||||
import { Avatar, createQuery } from '@hcengineering/presentation'
|
||||
import { Component, Label, showPanel } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
@ -42,11 +43,12 @@
|
||||
<Avatar avatar={object.avatar} size={'large'} icon={contact.icon.Company} />
|
||||
</div>
|
||||
{#if object}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="name lines-limit-2"
|
||||
class:over-underline={!disabled}
|
||||
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)}
|
||||
|
@ -14,13 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Person } from '@hcengineering/contact'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Class, Ref } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { PersonLabelTooltip } from '..'
|
||||
import PersonPresenter from './PersonPresenter.svelte'
|
||||
|
||||
export let value: Ref<Person> | null | undefined
|
||||
export let _class: Ref<Class<Person>> = contact.class.Person
|
||||
export let inline = false
|
||||
export let enlargedText = false
|
||||
export let isInteractive = true
|
||||
@ -34,7 +35,7 @@
|
||||
|
||||
let person: Person | undefined
|
||||
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 {
|
||||
if (value === undefined || value === null) {
|
||||
|
@ -67,7 +67,8 @@ export {
|
||||
EmployeeBrowser,
|
||||
MemberPresenter,
|
||||
EmployeeEditor,
|
||||
EmployeeAccountRefPresenter
|
||||
EmployeeAccountRefPresenter,
|
||||
EditPerson
|
||||
}
|
||||
|
||||
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
|
||||
|
@ -12,7 +12,7 @@
|
||||
"HaveWorkspace": "Уже есть рабочее пространство?",
|
||||
"LastName": "Фамилия",
|
||||
"FirstName": "Имя",
|
||||
"Join": "Присоедениться",
|
||||
"Join": "Присоединиться",
|
||||
"Email": "Email",
|
||||
"Password": "Пароль",
|
||||
"Workspace": "Рабочее пространство",
|
||||
|
@ -17,6 +17,7 @@
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import contact, { Channel, formatName, Person } from '@hcengineering/contact'
|
||||
import { ChannelsEditor } from '@hcengineering/contact-resources'
|
||||
import { Hierarchy } from '@hcengineering/core'
|
||||
import { Avatar, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Component, Label, showPanel } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
@ -47,12 +48,13 @@
|
||||
<div class="label uppercase"><Label label={recruit.string.Talent} /></div>
|
||||
<Avatar avatar={candidate?.avatar} size={'large'} />
|
||||
{#if candidate}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="name lines-limit-2"
|
||||
class:over-underline={!disabled}
|
||||
on:click={() => {
|
||||
if (!disabled && candidate) {
|
||||
showPanel(view.component.EditDoc, candidate._id, candidate._class, 'content')
|
||||
showPanel(view.component.EditDoc, candidate._id, Hierarchy.mixinOrClass(candidate), 'content')
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -14,16 +14,16 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
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 { createEventDispatcher, onMount } from 'svelte'
|
||||
import CandidateCard from './CandidateCard.svelte'
|
||||
import VacancyCard from './VacancyCard.svelte'
|
||||
import ExpandRightDouble from './icons/ExpandRightDouble.svelte'
|
||||
import VacancyCard from './VacancyCard.svelte'
|
||||
|
||||
import recruit from '../plugin'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import recruit from '../plugin'
|
||||
import Reviews from './review/Reviews.svelte'
|
||||
|
||||
export let object: Applicant
|
||||
|
@ -21,8 +21,7 @@
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Vacancy } from '@hcengineering/recruit'
|
||||
import { FullDescriptionBox } from '@hcengineering/text-editor'
|
||||
import tracker from '@hcengineering/tracker'
|
||||
import { Button, Component, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
|
||||
import { Button, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
|
||||
import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
@ -65,7 +64,6 @@
|
||||
<Panel
|
||||
icon={clazz.icon}
|
||||
title={object.name}
|
||||
subtitle={object.description}
|
||||
isHeader={true}
|
||||
isAside={true}
|
||||
{object}
|
||||
@ -73,6 +71,11 @@
|
||||
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}>
|
||||
{#if dir === 'column'}
|
||||
<div class="ac-subtitle">
|
||||
@ -129,9 +132,6 @@
|
||||
space={object.space}
|
||||
attachments={object.attachments ?? 0}
|
||||
/>
|
||||
<!-- <MembersBox label={recruit.string.Members} space={object} /> -->
|
||||
|
||||
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
|
||||
</Grid>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
@ -16,13 +16,13 @@
|
||||
import { AttachmentsPresenter } from '@hcengineering/attachment-resources'
|
||||
import { CommentsPresenter } from '@hcengineering/chunter-resources'
|
||||
import contact, { formatName } from '@hcengineering/contact'
|
||||
import type { WithLookup } from '@hcengineering/core'
|
||||
import { Hierarchy, WithLookup } from '@hcengineering/core'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { Avatar } from '@hcengineering/presentation'
|
||||
import type { Applicant, Candidate } from '@hcengineering/recruit'
|
||||
import task, { TodoItem } from '@hcengineering/task'
|
||||
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 ApplicationPresenter from './ApplicationPresenter.svelte'
|
||||
|
||||
@ -30,15 +30,13 @@
|
||||
export let dragged: boolean
|
||||
|
||||
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
|
||||
</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-between mb-3">
|
||||
<div class="flex-row-center">
|
||||
@ -72,18 +70,7 @@
|
||||
<div class="flex-row-center">
|
||||
<div class="sm-tool-icon step-lr75">
|
||||
<ApplicationPresenter value={object} />
|
||||
{#if todoItems.length > 0}
|
||||
<div
|
||||
class="ml-2"
|
||||
use:tooltip={{
|
||||
label: task.string.TodoItems,
|
||||
component: task.component.TodoItemsPopup,
|
||||
props: { value: object }
|
||||
}}
|
||||
>
|
||||
({doneTasks?.length}/{todoItems.length})
|
||||
</div>
|
||||
{/if}
|
||||
<Component is={tracker.component.RelatedIssueSelector} props={{ object }} />
|
||||
</div>
|
||||
{#if (object.attachments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
|
@ -16,6 +16,7 @@
|
||||
<script lang="ts">
|
||||
import calendar from '@hcengineering/calendar'
|
||||
import contact, { Contact } from '@hcengineering/contact'
|
||||
import { Hierarchy } from '@hcengineering/core'
|
||||
import { getClient, UserBox } from '@hcengineering/presentation'
|
||||
import type { Review } from '@hcengineering/recruit'
|
||||
import { FullDescriptionBox } from '@hcengineering/text-editor'
|
||||
@ -70,7 +71,7 @@
|
||||
class="clear-mins"
|
||||
on:click={() => {
|
||||
if (candidate !== undefined) {
|
||||
showPanel(view.component.EditDoc, candidate._id, candidate._class, 'content')
|
||||
showPanel(view.component.EditDoc, candidate._id, Hierarchy.mixinOrClass(candidate), 'content')
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -185,7 +185,7 @@
|
||||
<UpDownNavigator element={issue} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
<span class="fs-title">
|
||||
<span class="fs-title select-text-i">
|
||||
{#if issueId}{issueId}{/if}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
@ -265,7 +265,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span slot="actions-label">
|
||||
<span slot="actions-label" class="select-text">
|
||||
{#if issueId}{issueId}{/if}
|
||||
</span>
|
||||
<svelte:fragment slot="actions">
|
||||
|
@ -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}
|
@ -61,6 +61,7 @@ import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.sv
|
||||
import Views from './components/views/Views.svelte'
|
||||
import Statuses from './components/workflow/Statuses.svelte'
|
||||
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
|
||||
import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.svelte'
|
||||
import {
|
||||
getIssueId,
|
||||
getIssueTitle,
|
||||
@ -280,7 +281,8 @@ export default async (): Promise<Resources> => ({
|
||||
TeamPresenter,
|
||||
IssueStatistics,
|
||||
StatusRefPresenter,
|
||||
RelatedIssuesSection
|
||||
RelatedIssuesSection,
|
||||
RelatedIssueSelector
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||
|
@ -400,6 +400,7 @@ export default plugin(trackerId, {
|
||||
TrackerApp: '' as AnyComponent,
|
||||
RelatedIssues: '' as AnyComponent,
|
||||
RelatedIssuesSection: '' as AnyComponent,
|
||||
RelatedIssueSelector: '' as AnyComponent,
|
||||
RelatedIssueTemplates: '' as AnyComponent,
|
||||
EditIssue: '' as AnyComponent,
|
||||
CreateIssue: '' as AnyComponent,
|
||||
|
@ -35,8 +35,8 @@
|
||||
import { categorizeFields, getCollectionCounter, getFiltredKeys } from '../utils'
|
||||
import ActionContext from './ActionContext.svelte'
|
||||
import DocAttributeBar from './DocAttributeBar.svelte'
|
||||
import UpDownNavigator from './UpDownNavigator.svelte'
|
||||
import IconMixin from './icons/Mixin.svelte'
|
||||
import UpDownNavigator from './UpDownNavigator.svelte'
|
||||
|
||||
export let _id: Ref<Doc>
|
||||
export let _class: Ref<Class<Doc>>
|
||||
@ -147,19 +147,32 @@
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
async function getEditor (_class: Ref<Class<Doc>>): Promise<MixinEditor> {
|
||||
function getEditor (_class: Ref<Class<Doc>>): MixinEditor {
|
||||
const clazz = hierarchy.getClass(_class)
|
||||
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
|
||||
if (editorMixin?.editor == null && clazz.extends != null) return getEditor(clazz.extends)
|
||||
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
|
||||
|
||||
$: editorFooter = getEditorFooter(_class)
|
||||
|
||||
$: 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)
|
||||
mainEditor = await getEditor(_class)
|
||||
mainEditor = getEditor(_class)
|
||||
updateKeys(showAllMixins)
|
||||
}
|
||||
|
||||
@ -370,5 +383,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if editorFooter}
|
||||
<Component is={editorFooter.footer} props={{ object, _class, ...editorFooter.props }} />
|
||||
{/if}
|
||||
</Panel>
|
||||
{/if}
|
||||
|
@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 presentation, { getClient, ObjectCreate } from '@hcengineering/presentation'
|
||||
import {
|
||||
@ -151,7 +151,7 @@
|
||||
size={'small'}
|
||||
action={() => {
|
||||
if (selected) {
|
||||
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||
showPanel(view.component.EditDoc, selected._id, Hierarchy.mixinOrClass(selected), 'content')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -57,7 +57,7 @@
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: lookup = options?.lookup ?? buildConfigLookup(hierarchy, _class, config)
|
||||
$: lookup = buildConfigLookup(hierarchy, _class, config, options?.lookup)
|
||||
|
||||
let _sortKey = prefferedSorting
|
||||
$: if (!userSorting) {
|
||||
@ -109,7 +109,7 @@
|
||||
dispatch('content', objects)
|
||||
loading = loading === 1 ? 0 : -1
|
||||
},
|
||||
{ sort, limit: 200, lookup, ...options }
|
||||
{ sort, limit: 200, ...options, lookup }
|
||||
)
|
||||
if (update && ++loading > 0) {
|
||||
objects = []
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
const client = getClient()
|
||||
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
|
||||
.map((p) => {
|
||||
|
@ -68,7 +68,7 @@
|
||||
}
|
||||
|
||||
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[] = []
|
||||
for (const param of viewlet.config) {
|
||||
if (typeof param === 'string') {
|
||||
|
@ -48,8 +48,8 @@
|
||||
$: orderBy = viewOptions.orderBy
|
||||
|
||||
const docsQuery = createQuery()
|
||||
$: lookup = options?.lookup ?? buildConfigLookup(client.getHierarchy(), _class, config)
|
||||
$: resultOptions = { lookup, ...options, sort: { [orderBy[0]]: orderBy[1] } }
|
||||
$: lookup = buildConfigLookup(client.getHierarchy(), _class, config, options?.lookup)
|
||||
$: resultOptions = { ...options, lookup, sort: { [orderBy[0]]: orderBy[1] } }
|
||||
|
||||
let resultQuery: DocumentQuery<Doc> = query
|
||||
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => {
|
||||
|
@ -25,7 +25,9 @@ import core, {
|
||||
Obj,
|
||||
Ref,
|
||||
RefTo,
|
||||
TxOperations
|
||||
TxOperations,
|
||||
ReverseLookup,
|
||||
ReverseLookups
|
||||
} from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
@ -234,7 +236,8 @@ function getKeyLookup<T extends Doc> (
|
||||
export function buildConfigLookup<T extends Doc> (
|
||||
hierarchy: Hierarchy,
|
||||
_class: Ref<Class<T>>,
|
||||
config: Array<BuildModelKey | string>
|
||||
config: Array<BuildModelKey | string>,
|
||||
existingLookup?: Lookup<T>
|
||||
): Lookup<T> {
|
||||
let res: Lookup<T> = {}
|
||||
for (const key of config) {
|
||||
@ -244,6 +247,14 @@ export function buildConfigLookup<T extends Doc> (
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -168,6 +168,14 @@ export interface ObjectEditor extends Class<Doc> {
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ObjectEditorFooter extends Class<Doc> {
|
||||
editor: AnyComponent
|
||||
props?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -523,6 +531,7 @@ const view = plugin(viewId, {
|
||||
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>,
|
||||
ObjectPresenter: '' as Ref<Mixin<ObjectPresenter>>,
|
||||
ObjectEditorHeader: '' as Ref<Mixin<ObjectEditorHeader>>,
|
||||
ObjectEditorFooter: '' as Ref<Mixin<ObjectEditorFooter>>,
|
||||
ObjectValidator: '' as Ref<Mixin<ObjectValidator>>,
|
||||
ObjectFactory: '' as Ref<Mixin<ObjectFactory>>,
|
||||
ObjectTitle: '' as Ref<Mixin<ObjectTitle>>,
|
||||
|
@ -13,7 +13,7 @@
|
||||
"View": "Посмотреть",
|
||||
"Leave": "Покинуть",
|
||||
"Joined": "Вы присоеденились",
|
||||
"Join": "Присоедениться",
|
||||
"Join": "Присоединиться",
|
||||
"BrowseSpaces": "Обзор пространств",
|
||||
"AccountDisabled": "Аккаунт отключен",
|
||||
"AccountDisabledDescr": "Пожалуйста свяжитесь с администратором",
|
||||
|
@ -267,7 +267,7 @@ class TServerStorage implements ServerStorage {
|
||||
): Promise<FindResult<T>> {
|
||||
return await ctx.with('find-all', {}, (ctx) => {
|
||||
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('db-find-all', { _class: clazz, domain }, () =>
|
||||
|
@ -181,6 +181,7 @@ describe('mongo operations', () => {
|
||||
})
|
||||
|
||||
it('check add', async () => {
|
||||
jest.setTimeout(50000)
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
|
||||
name: `my-task-${i}`,
|
||||
|
Loading…
Reference in New Issue
Block a user