My assigned issues/applications (#1184)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-03-22 16:09:58 +07:00 committed by GitHub
parent 1993180fab
commit 1db0417e85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 204 additions and 19 deletions

View File

@ -183,6 +183,17 @@ export function createModel (builder: Builder): void {
label: recruit.string.SkillsLabel,
createItemLabel: recruit.string.SkillCreateLabel,
position: 'bottom'
},
{
id: 'assigned',
label: task.string.Assigned,
icon: task.icon.Task,
component: task.component.AssignedTasks,
componentProps: {
labelTasks: recruit.string.Applications,
_class: recruit.class.Applicant
},
position: 'top'
}
]
}
@ -206,8 +217,16 @@ export function createModel (builder: Builder): void {
'',
'title',
'city',
{ presenter: recruit.component.ApplicationsPresenter, label: recruit.string.ApplicationsShort, sortingKey: 'applications' },
{ presenter: attachment.component.AttachmentsPresenter, label: attachment.string.Files, sortingKey: 'attachments' },
{
presenter: recruit.component.ApplicationsPresenter,
label: recruit.string.ApplicationsShort,
sortingKey: 'applications'
},
{
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
{
// key: '$lookup.skills', // Required, since presenter require list of tag references or '' and TagsPopupPresenter
@ -244,7 +263,11 @@ export function createModel (builder: Builder): void {
'$lookup.assignee',
'$lookup.state',
'$lookup.doneState',
{ presenter: attachment.component.AttachmentsPresenter, label: attachment.string.Files, sortingKey: 'attachments' },
{
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
'modifiedOn',
'$lookup.attachedTo.$lookup.channels'
@ -279,7 +302,11 @@ export function createModel (builder: Builder): void {
'$lookup.assignee',
'$lookup.state',
'$lookup.doneState',
{ presenter: attachment.component.AttachmentsPresenter, label: attachment.string.Files, sortingKey: 'attachments' },
{
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
'modifiedOn',
'$lookup.attachedTo.$lookup.channels'
@ -399,8 +426,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: recruit.class.Vacancy,
action: recruit.action.EditVacancy,
query: {
}
query: {}
})
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.IgnoreActions, {

View File

@ -84,6 +84,7 @@ export class TLostState extends TDoneState implements LostState {}
* No domain is specified, since pure Tasks could not exists
*/
@Model(task.class.Task, core.class.AttachedDoc, DOMAIN_TASK, [task.interface.DocWithRank])
@UX(task.string.Task, task.icon.Task, task.string.Task)
export class TTask extends TAttachedDoc implements Task {
@Prop(TypeRef(task.class.State), task.string.TaskState)
state!: Ref<State>
@ -279,6 +280,15 @@ export function createModel (builder: Builder): void {
addSpaceLabel: task.string.CreateProject,
createComponent: task.component.CreateProject
}
],
specials: [
{
id: 'assigned',
label: task.string.Assigned,
icon: task.icon.Task,
component: task.component.AssignedTasks,
position: 'top'
}
]
}
},
@ -302,6 +312,10 @@ export function createModel (builder: Builder): void {
]
})
builder.mixin(task.class.Task, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.ObjectPresenter
})
builder.mixin(task.class.Issue, core.class.Class, view.mixin.AttributePresenter, {
presenter: task.component.TaskPresenter
})

View File

@ -66,7 +66,6 @@ export default mergeIds(taskId, task, {
Todo: '' as IntlString,
TaskDone: '' as IntlString,
TaskDueTo: '' as IntlString,
Task: '' as IntlString,
TaskParent: '' as IntlString,
IssueName: '' as IntlString,
TaskComments: '' as IntlString,

View File

@ -72,6 +72,8 @@
"StatusDelete": "Delete status",
"StatusDeleteConfirm": "Do you want to delete this status?",
"CantStatusDelete": "Can't delete status",
"CantStatusDeleteError": "There are objects in the given state. Move or delete them first."
"CantStatusDeleteError": "There are objects in the given state. Move or delete them first.",
"Tasks": "Tasks",
"Assigned": "Assigned to me"
}
}

View File

@ -72,6 +72,8 @@
"StatusDelete": "Удалить статус",
"StatusDeleteConfirm": "Вы действительно хотите удалить этот статус?",
"CantStatusDelete": "Невозможно удалить статус",
"CantStatusDeleteError": "Есть объекты с данным статусом. Сначала переместите или удалите их. "
"CantStatusDeleteError": "Есть объекты с данным статусом. Сначала переместите или удалите их. ",
"Tasks": "Задачи",
"Assigned": "Назначения"
}
}

View File

@ -47,6 +47,7 @@
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/chunter-resources": "~0.6.0",
"@anticrm/workbench": "~0.6.1",
"@anticrm/notification": "~0.6.0"
"@anticrm/notification": "~0.6.0",
"@anticrm/tags": "~0.6.1"
}
}

View File

@ -0,0 +1,129 @@
<!--
// 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 attachment from '@anticrm/attachment'
import chunter from '@anticrm/chunter'
import contact, { EmployeeAccount } from '@anticrm/contact'
import { Class, DocumentQuery, FindOptions, getCurrentAccount, Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import tags, { selectedTagElements, TagCategory, TagElement } from '@anticrm/tags'
import { DoneState, Task } from '@anticrm/task'
import { Component, Icon, Label, Scroller, SearchEdit } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import task from '../plugin'
export let _class: Ref<Class<Task>> = task.class.Task
export let labelTasks = task.string.Tasks
let search = ''
let resultQuery: DocumentQuery<Task> = {}
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 }
}
}
let doneStates: DoneState[] = []
const doneStateQuery = createQuery()
doneStateQuery.query(
task.class.DoneState,
{
},
(res) => (doneStates = res)
)
// Find all tags for object classe with matched elements
const query = createQuery()
$: query.query(tags.class.TagReference, { tag: { $in: $selectedTagElements } }, (result) => {
documentIds = Array.from(new Set<Ref<Task>>(result.filter(it => client.getHierarchy().isDerived(it.attachedToClass, _class)).map((it) => it.attachedTo as Ref<Task>)).values())
})
$: updateResultQuery(search, documentIds, doneStates)
function updateCategory (detail: { category: Ref<TagCategory> | null; elements: TagElement[] }) {
category = detail.category ?? undefined
selectedTagElements.set(Array.from(detail.elements ?? []).map((it) => it._id))
}
const taskOptions: FindOptions<Task> = {
lookup: {
attachedTo: [contact.class.Person, { _id: { channels: contact.class.Channel } }],
state: task.class.State,
assignee: contact.class.Employee,
doneState: task.class.DoneState
}
}
</script>
<div class="ac-header full">
<div class="ac-header__wrap-title">
<div class="ac-header__icon"><Icon icon={task.icon.Task} size={'small'} /></div>
<span class="ac-header__title"><Label label={labelTasks} /></span>
</div>
<SearchEdit
bind:value={search}
on:change={() => {
updateResultQuery(search, documentIds, doneStates)
}}
/>
</div>
<Component
is={tags.component.TagsCategoryBar}
props={{ targetClass: _class, category }}
on:change={(evt) => updateCategory(evt.detail)}
/>
<Scroller>
<Table
_class={_class}
config={[
'',
'$lookup.attachedTo',
'$lookup.assignee',
'$lookup.state',
'$lookup.doneState',
{
key: '',
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{
key: '',
presenter: chunter.component.CommentsPresenter,
label: chunter.string.Comments,
sortingKey: 'comments'
},
'modifiedOn'
]}
options={taskOptions}
query={resultQuery}
showNotification
highlightRows
/>
</Scroller>

View File

@ -39,6 +39,7 @@ import TemplatesIcon from './components/TemplatesIcon.svelte'
import TodoItemPresenter from './components/todos/TodoItemPresenter.svelte'
import Todos from './components/todos/Todos.svelte'
import TodoStatePresenter from './components/todos/TodoStatePresenter.svelte'
import AssignedTasks from './components/AssignedTasks.svelte'
import task from './plugin'
async function createTask (object: Doc): Promise<void> {
@ -145,7 +146,8 @@ export default async (): Promise<Resources> => ({
TaskHeader,
DoneStateEditor,
KanbanTemplateEditor,
KanbanTemplateSelector
KanbanTemplateSelector,
AssignedTasks
},
actionImpl: {
CreateTask: createTask,

View File

@ -68,12 +68,17 @@ export default mergeIds(taskId, task, {
CantStatusDelete: '' as IntlString,
CantStatusDeleteError: '' as IntlString,
Archive: '' as IntlString,
Unarchive: '' as IntlString
Unarchive: '' as IntlString,
Tasks: '' as IntlString,
Assigned: '' as IntlString,
Task: '' as IntlString
},
status: {
AssigneeRequired: '' as IntlString
},
component: {
TodoStatePresenter: '' as AnyComponent
TodoStatePresenter: '' as AnyComponent,
AssignedTasks: '' as AnyComponent
}
})

View File

@ -47,6 +47,7 @@
$: sortingFunction = (config.find(it => (typeof it !== 'string') && it.sortingKey === sortKey) as BuildModelKey)?.sortingFunction
let qindex = 0
async function update (
_class: Ref<Class<Doc>>,
query: DocumentQuery<Doc>,
@ -54,11 +55,15 @@
sortOrder: SortingOrder,
options?: FindOptions<Doc>
) {
const c = ++qindex
loading = true
q.query(
_class,
query,
(result) => {
if (c !== qindex) {
return // our data is invalid.
}
objects = result
if (sortingFunction !== undefined) {
const sf = sortingFunction

View File

@ -31,7 +31,7 @@
showPopup,
TooltipInstance
} from '@anticrm/ui'
import type { Application, NavigatorModel, ViewConfiguration } from '@anticrm/workbench'
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@anticrm/workbench'
import { onDestroy } from 'svelte'
import workbench from '../plugin'
import AccountPopup from './AccountPopup.svelte'
@ -51,7 +51,7 @@
let currentApp: Ref<Application> | undefined
let currentSpace: Ref<Space> | undefined
let currentSpecial: string | undefined
let specialComponent: AnyComponent | undefined
let specialComponent: SpecialNavModel | undefined
let currentApplication: Application | undefined
let currentView: ViewConfiguration | undefined
@ -134,9 +134,8 @@
navigate(loc)
}
function getSpecialComponent (id: string): AnyComponent | undefined {
const special = navigatorModel?.specials?.find((x) => x.id === id)
return special?.component
function getSpecialComponent (id: string): SpecialNavModel | undefined {
return navigatorModel?.specials?.find((x) => x.id === id)
}
let apps: Application[] = []
@ -295,7 +294,7 @@
{#if currentApplication && currentApplication.component}
<Component is={currentApplication.component} />
{:else if specialComponent}
<Component is={specialComponent} props={{ model: navigatorModel }} />
<Component is={specialComponent.component} props={{ model: navigatorModel, ...specialComponent.componentProps }} />
{:else}
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
{/if}

View File

@ -57,6 +57,7 @@ export interface SpecialNavModel {
label: IntlString
icon: Asset
component: AnyComponent
componentProps?: Record<string, string>
position?: 'top'|'bottom' // undefined == 'top
visibleIf?: Resource<(spaces: Space[]) => boolean>
// If defined, will be used to find spaces for visibleIf