Subissue estimations (#2254)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-08-22 08:03:32 +07:00 committed by GitHub
parent 854331ae5f
commit ccd2048ad0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 428 additions and 115 deletions

View File

@ -45,7 +45,7 @@ import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter' import chunter from '@anticrm/model-chunter'
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core' import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
import presentation from '@anticrm/model-presentation' import presentation from '@anticrm/model-presentation'
import view, { actionTemplates, createAction } from '@anticrm/model-view' import view, { actionTemplates, createAction, ViewAction } from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench' import workbench from '@anticrm/model-workbench'
import type { Asset, IntlString } from '@anticrm/platform' import type { Asset, IntlString } from '@anticrm/platform'
import setting from '@anticrm/setting' import setting from '@anticrm/setting'
@ -58,6 +58,7 @@ export const DOMAIN_CHANNEL = 'channel' as Domain
export class TChannelProvider extends TDoc implements ChannelProvider { export class TChannelProvider extends TDoc implements ChannelProvider {
label!: IntlString label!: IntlString
icon?: Asset icon?: Asset
action?: ViewAction
placeholder!: IntlString placeholder!: IntlString
} }
@ -305,7 +306,8 @@ export function createModel (builder: Builder): void {
{ {
label: contact.string.LinkedIn, label: contact.string.LinkedIn,
icon: contact.icon.LinkedIn, icon: contact.icon.LinkedIn,
placeholder: contact.string.LinkedInPlaceholder placeholder: contact.string.LinkedInPlaceholder,
action: contact.actionImpl.OpenChannel
}, },
contact.channelProvider.LinkedIn contact.channelProvider.LinkedIn
) )
@ -316,7 +318,8 @@ export function createModel (builder: Builder): void {
{ {
label: contact.string.Twitter, label: contact.string.Twitter,
icon: contact.icon.Twitter, icon: contact.icon.Twitter,
placeholder: contact.string.AtPlaceHolder placeholder: contact.string.AtPlaceHolder,
action: contact.actionImpl.OpenChannel
}, },
contact.channelProvider.Twitter contact.channelProvider.Twitter
) )
@ -327,7 +330,8 @@ export function createModel (builder: Builder): void {
{ {
label: contact.string.GitHub, label: contact.string.GitHub,
icon: contact.icon.GitHub, icon: contact.icon.GitHub,
placeholder: contact.string.AtPlaceHolder placeholder: contact.string.AtPlaceHolder,
action: contact.actionImpl.OpenChannel
}, },
contact.channelProvider.GitHub contact.channelProvider.GitHub
) )
@ -338,7 +342,8 @@ export function createModel (builder: Builder): void {
{ {
label: contact.string.Facebook, label: contact.string.Facebook,
icon: contact.icon.Facebook, icon: contact.icon.Facebook,
placeholder: contact.string.FacebookPlaceholder placeholder: contact.string.FacebookPlaceholder,
action: contact.actionImpl.OpenChannel
}, },
contact.channelProvider.Facebook contact.channelProvider.Facebook
) )
@ -349,7 +354,8 @@ export function createModel (builder: Builder): void {
{ {
label: contact.string.Homepage, label: contact.string.Homepage,
icon: contact.icon.Homepage, icon: contact.icon.Homepage,
placeholder: contact.string.HomepagePlaceholder placeholder: contact.string.HomepagePlaceholder,
action: contact.actionImpl.OpenChannel
}, },
contact.channelProvider.Homepage contact.channelProvider.Homepage
) )

View File

@ -83,6 +83,7 @@ export default mergeIds(contactId, contact, {
KickEmployee: '' as Ref<Action> KickEmployee: '' as Ref<Action>
}, },
actionImpl: { actionImpl: {
KickEmployee: '' as ViewAction KickEmployee: '' as ViewAction,
OpenChannel: '' as ViewAction
} }
}) })

View File

@ -307,6 +307,7 @@ export function createModel (builder: Builder): void {
label: recruit.string.Applications label: recruit.string.Applications
}, },
'$lookup.company', '$lookup.company',
'$lookup.company.$lookup.channels',
'location', 'location',
'description', 'description',
{ {

View File

@ -55,7 +55,8 @@ import {
SprintStatus, SprintStatus,
Team, Team,
TimeSpendReport, TimeSpendReport,
trackerId trackerId,
IssueChildInfo
} from '@anticrm/tracker' } from '@anticrm/tracker'
import { KeyBinding } from '@anticrm/view' import { KeyBinding } from '@anticrm/view'
import tracker from './plugin' import tracker from './plugin'
@ -238,6 +239,8 @@ export class TIssue extends TAttachedDoc implements Issue {
@Prop(Collection(tracker.class.TimeSpendReport), tracker.string.TimeSpendReports) @Prop(Collection(tracker.class.TimeSpendReport), tracker.string.TimeSpendReports)
reports!: number reports!: number
declare childInfo: IssueChildInfo[]
} }
/** /**

View File

@ -32,7 +32,11 @@ function $push (document: Doc, keyval: Record<string, PropertyType>): void {
if (typeof val === 'object') { if (typeof val === 'object') {
const arr = doc[key] as Array<any> const arr = doc[key] as Array<any>
const desc = val as Position<PropertyType> const desc = val as Position<PropertyType>
arr.splice(desc.$position, 0, ...desc.$each) if ('$each' in desc) {
arr.splice(desc.$position ?? 0, 0, ...desc.$each)
} else {
arr.push(val)
}
} else { } else {
doc[key].push(val) doc[key].push(val)
} }
@ -48,7 +52,20 @@ function $pull (document: Doc, keyval: Record<string, PropertyType>): void {
const arr = doc[key] as Array<any> const arr = doc[key] as Array<any>
if (typeof keyval[key] === 'object') { if (typeof keyval[key] === 'object') {
const { $in } = keyval[key] as PullArray<PropertyType> const { $in } = keyval[key] as PullArray<PropertyType>
doc[key] = arr.filter((val) => !$in.includes(val))
doc[key] = arr.filter((val) => {
if ($in !== undefined) {
return !$in.includes(val)
} else {
// We need to match all fields
for (const [kk, kv] of Object.entries(keyval[key])) {
if (val[kk] !== kv) {
return true
}
}
return false
}
})
} else { } else {
doc[key] = arr.filter((val) => val !== keyval[key]) doc[key] = arr.filter((val) => val !== keyval[key])
} }

View File

@ -103,7 +103,7 @@ export interface TxMixin<D extends Doc, M extends D> extends TxCUD<D> {
* @public * @public
*/ */
export type ArrayAsElement<T> = { export type ArrayAsElement<T> = {
[P in keyof T]: T[P] extends Arr<infer X> ? X | PullArray<X> : never [P in keyof T]: T[P] extends Arr<infer X> ? Partial<X> | PullArray<X> : never
} }
/** /**

View File

@ -596,10 +596,12 @@ export class LiveQuery extends TxProcessor implements Client {
;(result as any)[key] = objects[0] ;(result as any)[key] = objects[0]
const nestedResult = {} const nestedResult = {}
const parent = (result as any)[key] const parent = (result as any)[key]
if (parent !== undefined) {
await this.getLookupValue(_class, parent, nested, nestedResult) await this.getLookupValue(_class, parent, nested, nestedResult)
Object.assign(parent, { Object.assign(parent, {
$lookup: nestedResult $lookup: nestedResult
}) })
}
} else { } else {
const objects = await this.findAll(value, { _id: getObjectValue(tkey, doc) }) const objects = await this.findAll(value, { _id: getObjectValue(tkey, doc) })
;(result as any)[key] = objects[0] ;(result as any)[key] = objects[0]

View File

@ -22,9 +22,9 @@
</script> </script>
{#if attachments.length} {#if attachments.length}
<div class="flex-col"> <div class="flex flex-wrap">
{#each attachments as attachment} {#each attachments as attachment}
<div class="step-tb75"> <div class="p-2">
<AttachmentPreview value={attachment} isSaved={savedAttachmentsIds?.includes(attachment._id) ?? false} /> <AttachmentPreview value={attachment} isSaved={savedAttachmentsIds?.includes(attachment._id) ?? false} />
</div> </div>
{/each} {/each}

View File

@ -13,23 +13,22 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount, afterUpdate } from 'svelte'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform' import { translate } from '@anticrm/platform'
import type { PopupOptions } from '@anticrm/ui'
import { import {
Button, Button,
IconClose,
closeTooltip,
IconBlueCheck,
registerFocus,
createFocusManager, createFocusManager,
FocusHandler,
IconArrowRight, IconArrowRight,
IconEdit IconBlueCheck,
IconClose,
IconEdit,
registerFocus
} from '@anticrm/ui' } from '@anticrm/ui'
import IconCopy from './icons/Copy.svelte' import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
import { FocusHandler } from '@anticrm/ui'
import type { PopupOptions } from '@anticrm/ui'
import plugin from '../plugin' import plugin from '../plugin'
import IconCopy from './icons/Copy.svelte'
export let value: string = '' export let value: string = ''
export let placeholder: IntlString export let placeholder: IntlString
@ -101,8 +100,9 @@
style="width: 100%;" style="width: 100%;"
on:keypress={(ev) => { on:keypress={(ev) => {
if (ev.key === 'Enter') { if (ev.key === 'Enter') {
ev.preventDefault()
ev.stopPropagation()
dispatch('close', value) dispatch('close', value)
closeTooltip()
} }
}} }}
on:change on:change

View File

@ -32,6 +32,7 @@
Menu, Menu,
showPopup showPopup
} from '@anticrm/ui' } from '@anticrm/ui'
import { ViewAction } from '@anticrm/view'
import { createEventDispatcher, tick } from 'svelte' import { createEventDispatcher, tick } from 'svelte'
import { getChannelProviders } from '../utils' import { getChannelProviders } from '../utils'
import ChannelEditor from './ChannelEditor.svelte' import ChannelEditor from './ChannelEditor.svelte'
@ -55,6 +56,7 @@
icon: Asset icon: Asset
value: string value: string
presenter?: AnyComponent presenter?: AnyComponent
action?: ViewAction
placeholder: IntlString placeholder: IntlString
provider: Ref<ChannelProvider> provider: Ref<ChannelProvider>
integration: boolean integration: boolean
@ -74,6 +76,7 @@
icon: provider.icon as Asset, icon: provider.icon as Asset,
value: item.value, value: item.value,
presenter: provider.presenter, presenter: provider.presenter,
action: provider.action,
placeholder: provider.placeholder, placeholder: provider.placeholder,
provider: provider._id, provider: provider._id,
notification, notification,

View File

@ -16,8 +16,10 @@
<script lang="ts"> <script lang="ts">
import type { Channel } from '@anticrm/contact' import type { Channel } from '@anticrm/contact'
import { Doc } from '@anticrm/core' import { Doc } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import type { ButtonKind, ButtonSize } from '@anticrm/ui' import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import { showPopup } from '@anticrm/ui' import { showPopup } from '@anticrm/ui'
import { ViewAction } from '@anticrm/view'
import ChannelsDropdown from './ChannelsDropdown.svelte' import ChannelsDropdown from './ChannelsDropdown.svelte'
export let value: Channel[] | Channel | null export let value: Channel[] | Channel | null
@ -29,12 +31,16 @@
export let shape: 'circle' | undefined = 'circle' export let shape: 'circle' | undefined = 'circle'
export let object: Doc export let object: Doc
function _open (ev: any) { async function _open (ev: CustomEvent): Promise<void> {
if (ev.detail.presenter !== undefined && Array.isArray(value)) { if (ev.detail.presenter !== undefined && Array.isArray(value)) {
const channel = value[0]
if (channel !== undefined) {
showPopup(ev.detail.presenter, { _id: object._id, _class: object._class }, 'float') showPopup(ev.detail.presenter, { _id: object._id, _class: object._class }, 'float')
} }
if (ev.detail.action !== undefined && Array.isArray(value)) {
const action = await getResource(ev.detail.action as ViewAction)
const channel = value.find((it) => it.value === ev.detail.value)
if (action !== undefined && channel !== undefined) {
action(channel, ev)
}
} }
} }
</script> </script>

View File

@ -14,41 +14,41 @@
// limitations under the License. // limitations under the License.
// //
import { Contact, Employee, formatName } from '@anticrm/contact' import { Channel, Contact, Employee, formatName } from '@anticrm/contact'
import { Class, Client, Ref } from '@anticrm/core' import { Class, Client, Ref } from '@anticrm/core'
import { leaveWorkspace } from '@anticrm/login-resources'
import { Resources } from '@anticrm/platform' import { Resources } from '@anticrm/platform'
import { Avatar, getClient, MessageBox, ObjectSearchResult, UserInfo } from '@anticrm/presentation' import { Avatar, getClient, MessageBox, ObjectSearchResult, UserInfo } from '@anticrm/presentation'
import { showPopup } from '@anticrm/ui' import { showPopup } from '@anticrm/ui'
import Channels from './components/Channels.svelte' import Channels from './components/Channels.svelte'
import ChannelsDropdown from './components/ChannelsDropdown.svelte'
import ChannelsEditor from './components/ChannelsEditor.svelte' import ChannelsEditor from './components/ChannelsEditor.svelte'
import ChannelsPresenter from './components/ChannelsPresenter.svelte' import ChannelsPresenter from './components/ChannelsPresenter.svelte'
import ChannelsView from './components/ChannelsView.svelte' import ChannelsView from './components/ChannelsView.svelte'
import ChannelsDropdown from './components/ChannelsDropdown.svelte'
import ContactPresenter from './components/ContactPresenter.svelte' import ContactPresenter from './components/ContactPresenter.svelte'
import Contacts from './components/Contacts.svelte' import Contacts from './components/Contacts.svelte'
import CreateEmployee from './components/CreateEmployee.svelte'
import CreateOrganization from './components/CreateOrganization.svelte' import CreateOrganization from './components/CreateOrganization.svelte'
import CreateOrganizations from './components/CreateOrganizations.svelte' import CreateOrganizations from './components/CreateOrganizations.svelte'
import CreatePerson from './components/CreatePerson.svelte' import CreatePerson from './components/CreatePerson.svelte'
import CreatePersons from './components/CreatePersons.svelte' import CreatePersons from './components/CreatePersons.svelte'
import EditMember from './components/EditMember.svelte'
import EditOrganization from './components/EditOrganization.svelte' import EditOrganization from './components/EditOrganization.svelte'
import EditPerson from './components/EditPerson.svelte' import EditPerson from './components/EditPerson.svelte'
import EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svelte'
import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte'
import EmployeeBrowser from './components/EmployeeBrowser.svelte'
import EmployeeEditor from './components/EmployeeEditor.svelte'
import EmployeePresenter from './components/EmployeePresenter.svelte'
import MemberPresenter from './components/MemberPresenter.svelte'
import Members from './components/Members.svelte'
import OrganizationEditor from './components/OrganizationEditor.svelte'
import OrganizationPresenter from './components/OrganizationPresenter.svelte' import OrganizationPresenter from './components/OrganizationPresenter.svelte'
import OrganizationSelector from './components/OrganizationSelector.svelte'
import PersonEditor from './components/PersonEditor.svelte'
import PersonPresenter from './components/PersonPresenter.svelte' import PersonPresenter from './components/PersonPresenter.svelte'
import SocialEditor from './components/SocialEditor.svelte' import SocialEditor from './components/SocialEditor.svelte'
import contact from './plugin' import contact from './plugin'
import EmployeePresenter from './components/EmployeePresenter.svelte'
import EmployeeBrowser from './components/EmployeeBrowser.svelte'
import EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svelte'
import OrganizationEditor from './components/OrganizationEditor.svelte'
import PersonEditor from './components/PersonEditor.svelte'
import OrganizationSelector from './components/OrganizationSelector.svelte'
import Members from './components/Members.svelte'
import MemberPresenter from './components/MemberPresenter.svelte'
import EditMember from './components/EditMember.svelte'
import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte'
import EmployeeEditor from './components/EmployeeEditor.svelte'
import CreateEmployee from './components/CreateEmployee.svelte'
import { leaveWorkspace } from '@anticrm/login-resources'
export { export {
Channels, Channels,
@ -98,10 +98,16 @@ async function kickEmployee (doc: Employee): Promise<void> {
} }
) )
} }
async function openChannelURL (doc: Channel): Promise<void> {
if (doc.value.startsWith('http://') || doc.value.startsWith('https://')) {
window.open(doc.value)
}
}
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
actionImpl: { actionImpl: {
KickEmployee: kickEmployee KickEmployee: kickEmployee,
OpenChannel: openChannelURL
}, },
component: { component: {
PersonEditor, PersonEditor,

View File

@ -30,7 +30,7 @@ import {
import type { Asset, Plugin } from '@anticrm/platform' import type { Asset, Plugin } from '@anticrm/platform'
import { IntlString, plugin } from '@anticrm/platform' import { IntlString, plugin } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui' import type { AnyComponent } from '@anticrm/ui'
import { Viewlet } from '@anticrm/view' import { ViewAction, Viewlet } from '@anticrm/view'
/** /**
* @public * @public
@ -46,8 +46,16 @@ export interface Persons extends Space {}
* @public * @public
*/ */
export interface ChannelProvider extends Doc, UXObject { export interface ChannelProvider extends Doc, UXObject {
// Placeholder
placeholder: IntlString placeholder: IntlString
// Presenter will be shown on click for channel
presenter?: AnyComponent presenter?: AnyComponent
// Action to be performed if there is no presenter defined.
action?: ViewAction
// Integration type
integrationType?: Ref<Doc> integrationType?: Ref<Doc>
} }

View File

@ -208,7 +208,9 @@
"TimeSpendReportValueTooltip": "Reported time in man days", "TimeSpendReportValueTooltip": "Reported time in man days",
"TimeSpendReportDescription": "Description", "TimeSpendReportDescription": "Description",
"TimeSpendValue": "{value}d", "TimeSpendValue": "{value}d",
"SprintPassed": "{from}d/{to}d" "SprintPassed": "{from}d/{to}d",
"ChildEstimation": "Subissues Estimation",
"ChildReportedTime": "Subissues Time"
}, },
"status": {} "status": {}
} }

View File

@ -208,7 +208,9 @@
"TimeSpendReportValueTooltip": "Затраченное время в человеко днях", "TimeSpendReportValueTooltip": "Затраченное время в человеко днях",
"TimeSpendReportDescription": "Описание", "TimeSpendReportDescription": "Описание",
"TimeSpendValue": "{value}d", "TimeSpendValue": "{value}d",
"SprintPassed": "{from}d/{to}d" "SprintPassed": "{from}d/{to}d",
"ChildEstimation": "Subissues Estimation",
"ChildReportedTime": "Subissues Time"
}, },
"status": {} "status": {}
} }

View File

@ -42,6 +42,7 @@
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte' import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SprintSelector from './sprints/SprintSelector.svelte' import SprintSelector from './sprints/SprintSelector.svelte'
import { activeProject, activeSprint } from '../issues' import { activeProject, activeSprint } from '../issues'
import EstimationEditor from './issues/timereport/EstimationEditor.svelte'
export let space: Ref<Team> export let space: Ref<Team>
export let status: Ref<IssueStatus> | undefined = undefined export let status: Ref<IssueStatus> | undefined = undefined
@ -71,7 +72,8 @@
parents: [], parents: [],
reportedTime: 0, reportedTime: 0,
estimation: 0, estimation: 0,
reports: 0 reports: 0,
childInfo: []
} }
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -156,8 +158,9 @@
? [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents] ? [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
: [], : [],
reportedTime: 0, reportedTime: 0,
estimation: 0, estimation: object.estimation,
reports: 0 reports: 0,
childInfo: []
} }
await client.addCollection( await client.addCollection(
@ -344,6 +347,7 @@
labels = labels.filter((it) => it._id !== evt.detail) labels = labels.filter((it) => it._id !== evt.detail)
}} }}
/> />
<EstimationEditor kind={'no-border'} size={'small'} value={object} />
<ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} /> <ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} />
<SprintSelector value={object.sprint} onSprintIdChange={handleSprintIdChanged} /> <SprintSelector value={object.sprint} onSprintIdChange={handleSprintIdChanged} />
{#if object.dueDate !== null} {#if object.dueDate !== null}

View File

@ -21,8 +21,12 @@
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
// export let inline: boolean = false // export let inline: boolean = false
export let disableClick = false
function handleIssueEditorOpened () { function handleIssueEditorOpened () {
if (disableClick) {
return
}
showPanel(tracker.component.EditIssue, value._id, value._class, 'content') showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
} }
@ -39,7 +43,7 @@
</script> </script>
{#if value} {#if value}
<span class="issuePresenterRoot" title="title" on:click={handleIssueEditorOpened}> <span class="issuePresenterRoot" class:noPointer={disableClick} title="title" on:click={handleIssueEditorOpened}>
{title} {title}
</span> </span>
{/if} {/if}
@ -56,6 +60,10 @@
color: var(--content-color); color: var(--content-color);
cursor: pointer; cursor: pointer;
&.noPointer {
cursor: default;
}
&:hover { &:hover {
color: var(--caption-color); color: var(--caption-color);
text-decoration: underline; text-decoration: underline;

View File

@ -24,6 +24,7 @@
import AssigneeEditor from '../AssigneeEditor.svelte' import AssigneeEditor from '../AssigneeEditor.svelte'
import StatusEditor from '../StatusEditor.svelte' import StatusEditor from '../StatusEditor.svelte'
import PriorityEditor from '../PriorityEditor.svelte' import PriorityEditor from '../PriorityEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte'
export let parentIssue: Issue export let parentIssue: Issue
export let issueStatuses: WithLookup<IssueStatus>[] export let issueStatuses: WithLookup<IssueStatus>[]
@ -55,7 +56,12 @@
dueDate: null, dueDate: null,
comments: 0, comments: 0,
subIssues: 0, subIssues: 0,
parents: [] parents: [],
sprint: parentIssue.sprint,
estimation: 0,
reportedTime: 0,
reports: 0,
childInfo: []
} }
} }
@ -209,6 +215,7 @@
labels = labels.filter((it) => it._id !== evt.detail) labels = labels.filter((it) => it._id !== evt.detail)
}} }}
/> />
<EstimationEditor kind={'no-border'} size={'small'} value={newIssue} />
</div> </div>
<div class="buttons-group small-gap"> <div class="buttons-group small-gap">
<Button label={presentation.string.Cancel} size="small" kind="transparent" on:click={close} /> <Button label={presentation.string.Cancel} size="small" kind="transparent" on:click={close} />

View File

@ -279,7 +279,9 @@
<SubIssues {issue} {issueStatuses} {currentTeam} /> <SubIssues {issue} {issueStatuses} {currentTeam} />
{/key} {/key}
</div> </div>
<div class="mt-6">
<AttachmentDocList value={issue} /> <AttachmentDocList value={issue} />
</div>
{/if} {/if}
<span slot="actions-label"> <span slot="actions-label">

View File

@ -32,6 +32,7 @@
import DueDateEditor from '../DueDateEditor.svelte' import DueDateEditor from '../DueDateEditor.svelte'
import PriorityEditor from '../PriorityEditor.svelte' import PriorityEditor from '../PriorityEditor.svelte'
import StatusEditor from '../StatusEditor.svelte' import StatusEditor from '../StatusEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte'
export let issues: Issue[] export let issues: Issue[]
export let issueStatuses: WithLookup<IssueStatus>[] export let issueStatuses: WithLookup<IssueStatus>[]
@ -148,6 +149,7 @@
</span> </span>
</div> </div>
<div class="flex-center flex-no-shrink"> <div class="flex-center flex-no-shrink">
<EstimationEditor value={issue} kind={'list'} />
{#if issue.dueDate !== null} {#if issue.dueDate !== null}
<DueDateEditor value={issue} /> <DueDateEditor value={issue} />
{/if} {/if}

View File

@ -13,15 +13,18 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { AttachedData } from '@anticrm/core'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import { Issue } from '@anticrm/tracker' import { Issue } from '@anticrm/tracker'
import { Button, ButtonKind, ButtonSize, eventToHTMLElement, Label, showPopup } from '@anticrm/ui' import { Button, ButtonKind, ButtonSize, eventToHTMLElement, Label, showPopup } from '@anticrm/ui'
import EditBoxPopup from '@anticrm/view-resources/src/components/EditBoxPopup.svelte'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import EstimationPopup from './EstimationPopup.svelte' import EstimationPopup from './EstimationPopup.svelte'
import EstimationProgressCircle from './EstimationProgressCircle.svelte' import EstimationProgressCircle from './EstimationProgressCircle.svelte'
export let value: Issue export let value: Issue | AttachedData<Issue>
export let isEditable: boolean = true export let isEditable: boolean = true
export let kind: ButtonKind = 'link' export let kind: ButtonKind = 'link'
@ -39,6 +42,7 @@
return return
} }
if (kind === 'list') {
showPopup( showPopup(
EstimationPopup, EstimationPopup,
{ value: value.estimation, format: 'number', object: value }, { value: value.estimation, format: 'number', object: value },
@ -49,6 +53,13 @@
} }
} }
) )
} else {
showPopup(EditBoxPopup, { value, format: 'number' }, eventToHTMLElement(event), (res) => {
if (res !== undefined) {
changeEstimation(res)
}
})
}
} }
const changeEstimation = async (newEstimation: number | undefined) => { const changeEstimation = async (newEstimation: number | undefined) => {
@ -60,22 +71,67 @@
if ('_id' in value) { if ('_id' in value) {
await client.update(value, { estimation: newEstimation }) await client.update(value, { estimation: newEstimation })
} else {
value.estimation = newEstimation
} }
} }
$: childReportTime = (value.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0)
$: childEstimationTime = (value.childInfo ?? []).map((it) => it.estimation).reduce((a, b) => a + b, 0)
function hourFloor (value: number): number {
const days = Math.ceil(value)
const hours = value - days
return days + Math.floor(hours * 10) / 10
}
</script> </script>
{#if value} {#if value}
{#if kind === 'list'} {#if kind === 'list'}
<div class="estimation-container" on:click={handleestimationEditorOpened}> <div class="estimation-container" on:click={handleestimationEditorOpened}>
<div class="icon"> <div class="icon">
<EstimationProgressCircle value={value.reportedTime} max={value.estimation} /> <EstimationProgressCircle value={Math.max(value.reportedTime, childReportTime)} max={value.estimation} />
</div> </div>
<span class="overflow-label label"> <span class="overflow-label label flex-row-center flex-nowrap text-md">
{#if value.reportedTime > 0} {#if value.reportedTime > 0 || childReportTime > 0}
<Label label={tracker.string.TimeSpendValue} params={{ value: value.reportedTime }} /> {#if childReportTime}
/ {@const rchildReportTime = hourFloor(childReportTime)}
{@const reportDiff = rchildReportTime - hourFloor(value.reportedTime)}
{#if reportDiff !== 0 && value.reportedTime !== 0}
<div class="flex flex-nowrap mr-1" class:showError={reportDiff > 0}>
<Label label={tracker.string.TimeSpendValue} params={{ value: rchildReportTime }} />
</div>
<div class="romColor">
(<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />)
</div>
{:else if value.reportedTime === 0}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(childReportTime) }} />
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />
{/if}
<div class="p-1">/</div>
{/if}
{#if childEstimationTime}
{@const childEstTime = Math.round(childEstimationTime)}
{@const estimationDiff = childEstTime - Math.round(value.estimation)}
{#if estimationDiff !== 0}
<div class="flex flex-nowrap mr-1" class:showWarning={estimationDiff !== 0}>
<Label label={tracker.string.TimeSpendValue} params={{ value: childEstTime }} />
</div>
{#if value.estimation !== 0}
<div class="romColor">
(<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />)
</div>
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />
{/if} {/if}
<Label label={tracker.string.TimeSpendValue} params={{ value: value.estimation }} />
</span> </span>
</div> </div>
{:else} {:else}
@ -122,5 +178,15 @@
color: var(--caption-color) !important; color: var(--caption-color) !important;
} }
} }
.showError {
color: var(--error-color) !important;
}
.showWarning {
color: var(--warning-color) !important;
}
.romColor {
color: var(--content-color) !important;
}
} }
</style> </style>

View File

@ -16,14 +16,15 @@
<script lang="ts"> <script lang="ts">
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import { FindOptions } from '@anticrm/core' import { FindOptions } from '@anticrm/core'
import { Card } from '@anticrm/presentation' import presentation, { Card } from '@anticrm/presentation'
import { Issue, TimeSpendReport } from '@anticrm/tracker' import { Issue, TimeSpendReport } from '@anticrm/tracker'
import { Button, EditBox, EditStyle, eventToHTMLElement, IconAdd, Scroller, showPopup } from '@anticrm/ui' import { Button, EditBox, EditStyle, eventToHTMLElement, IconAdd, Label, Scroller, showPopup } from '@anticrm/ui'
import { TableBrowser } from '@anticrm/view-resources' import { TableBrowser } from '@anticrm/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import IssuePresenter from '../IssuePresenter.svelte'
import ParentNamesPresenter from '../ParentNamesPresenter.svelte'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte' import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
import presentation from '@anticrm/presentation'
export let value: string | number | undefined export let value: string | number | undefined
export let format: 'text' | 'password' | 'number' export let format: 'text' | 'password' | 'number'
@ -39,8 +40,9 @@
if (ev.key === 'Enter') dispatch('close', _value) if (ev.key === 'Enter') dispatch('close', _value)
} }
const options: FindOptions<TimeSpendReport> = { const options: FindOptions<TimeSpendReport> = {
lookup: { employee: contact.class.Employee } lookup: { employee: contact.class.Employee, attachedTo: tracker.class.Issue }
} }
$: childIds = Array.from((object.childInfo ?? []).map((it) => it.childId))
</script> </script>
<Card <Card
@ -54,6 +56,9 @@
dispatch('close', null) dispatch('close', null)
}} }}
> >
<svelte:fragment slot="header">
<IssuePresenter value={object} disableClick />
</svelte:fragment>
<div class="header no-border flex-col p-1"> <div class="header no-border flex-col p-1">
<div class="flex-row-center flex-between"> <div class="flex-row-center flex-between">
<EditBox <EditBox
@ -67,11 +72,28 @@
/> />
</div> </div>
</div> </div>
<Label label={tracker.string.ChildEstimation} />:
<Scroller tableFade>
<TableBrowser
_class={tracker.class.Issue}
query={{ _id: { $in: childIds } }}
config={['', { key: '$lookup.attachedTo', presenter: ParentNamesPresenter }, 'estimation']}
{options}
/>
</Scroller>
<Label label={tracker.string.ReportedTime} />:
<Scroller tableFade> <Scroller tableFade>
<TableBrowser <TableBrowser
_class={tracker.class.TimeSpendReport} _class={tracker.class.TimeSpendReport}
query={{ attachedTo: object._id }} query={{ attachedTo: { $in: [object._id, ...childIds] } }}
config={['', '$lookup.employee', 'date', 'description']} config={[
'$lookup.attachedTo',
{ key: '$lookup.attachedTo', presenter: ParentNamesPresenter },
'',
'$lookup.employee',
'date',
'description'
]}
{options} {options}
/> />
</Scroller> </Scroller>

View File

@ -25,9 +25,6 @@
export let greenColor: string = FernColor export let greenColor: string = FernColor
export let overdueColor = FlamingoColor export let overdueColor = FlamingoColor
if (value > max) value = max
if (value < min) value = min
const lenghtC: number = Math.PI * 14 - 1 const lenghtC: number = Math.PI * 14 - 1
$: procC = lenghtC / (max - min) $: procC = lenghtC / (max - min)
$: dashOffset = (Math.min(value, max) - min) * procC $: dashOffset = (Math.min(value, max) - min) * procC

View File

@ -36,12 +36,18 @@
function showReports (event: MouseEvent): void { function showReports (event: MouseEvent): void {
showPopup(ReportsPopup, { issue: object }, eventToHTMLElement(event)) showPopup(ReportsPopup, { issue: object }, eventToHTMLElement(event))
} }
$: childTime = (object.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0)
</script> </script>
{#if kind === 'link'} {#if kind === 'link'}
<div class="link-container flex-between" on:click={showReports}> <div class="link-container flex-between" on:click={showReports}>
{#if value !== undefined} {#if value !== undefined}
<span class="overflow-label">{value}</span> <span class="overflow-label">
{value}
{#if childTime !== 0}
/ {childTime}
{/if}
</span>
{:else} {:else}
<span class="dark-color"><Label label={placeholder} /></span> <span class="dark-color"><Label label={placeholder} /></span>
{/if} {/if}
@ -50,7 +56,12 @@
</div> </div>
</div> </div>
{:else if value !== undefined} {:else if value !== undefined}
<span class="overflow-label">{value}</span> <span class="overflow-label">
{value}
{#if childTime !== 0}
/ {childTime}
{/if}
</span>
{:else} {:else}
<span class="dark-color"><Label label={placeholder} /></span> <span class="dark-color"><Label label={placeholder} /></span>
{/if} {/if}

View File

@ -20,6 +20,7 @@
import { Button, eventToHTMLElement, IconAdd, Scroller, showPopup } from '@anticrm/ui' import { Button, eventToHTMLElement, IconAdd, Scroller, showPopup } from '@anticrm/ui'
import { TableBrowser } from '@anticrm/view-resources' import { TableBrowser } from '@anticrm/view-resources'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import IssuePresenter from '../IssuePresenter.svelte'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte' import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
export let issue: Issue export let issue: Issue
@ -27,7 +28,10 @@
return true return true
} }
const options: FindOptions<TimeSpendReport> = { const options: FindOptions<TimeSpendReport> = {
lookup: { employee: contact.class.Employee } lookup: {
attachedTo: tracker.class.Issue,
employee: contact.class.Employee
}
} }
function addReport (event: MouseEvent): void { function addReport (event: MouseEvent): void {
showPopup( showPopup(
@ -45,11 +49,23 @@
okAction={() => {}} okAction={() => {}}
okLabel={presentation.string.Ok} okLabel={presentation.string.Ok}
> >
<svelte:fragment slot="header">
<IssuePresenter value={issue} disableClick />
</svelte:fragment>
<Scroller tableFade> <Scroller tableFade>
<TableBrowser <TableBrowser
_class={tracker.class.TimeSpendReport} _class={tracker.class.TimeSpendReport}
query={{ attachedTo: issue._id }} query={{ attachedTo: { $in: [issue._id, ...issue.childInfo.map((it) => it.childId)] } }}
config={['', '$lookup.employee', 'description', 'date', 'modifiedOn', 'modifiedBy']} config={[
'$lookup.attachedTo',
'$lookup.attachedTo.title',
'',
'$lookup.employee',
'description',
'date',
'modifiedOn',
'modifiedBy'
]}
{options} {options}
/> />
</Scroller> </Scroller>

View File

@ -55,12 +55,15 @@
) )
} }
$: totalEstimation = (issues ?? [{ estimation: 0 }]) $: ids = new Set(issues?.map((it) => it._id) ?? [])
$: noParents = issues?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
$: totalEstimation = (noParents ?? [{ estimation: 0 }])
.map((it) => it.estimation) .map((it) => it.estimation)
.reduce((it, cur) => { .reduce((it, cur) => {
return it + cur return it + cur
}) })
$: totalReported = (issues ?? [{ reportedTime: 0 }]) $: totalReported = (noParents ?? [{ reportedTime: 0 }])
.map((it) => it.reportedTime) .map((it) => it.reportedTime)
.reduce((it, cur) => { .reduce((it, cur) => {
return it + cur return it + cur
@ -108,6 +111,7 @@
{@const now = Date.now()} {@const now = Date.now()}
{#if sprint.startDate < now && now < sprint.targetDate} {#if sprint.startDate < now && now < sprint.targetDate}
<!-- Active sprint in time --> <!-- Active sprint in time -->
<div class="ml-2">
<Label <Label
label={tracker.string.SprintPassed} label={tracker.string.SprintPassed}
params={{ params={{
@ -115,6 +119,7 @@
to: getDayOfSprint(sprint.startDate, sprint.targetDate) - 1 to: getDayOfSprint(sprint.startDate, sprint.targetDate) - 1
}} }}
/> />
</div>
{/if} {/if}
{/if} {/if}
<!-- <Label label={tracker.string.SprintDay} value={}/> --> <!-- <Label label={tracker.string.SprintDay} value={}/> -->

View File

@ -224,7 +224,10 @@ export default mergeIds(trackerId, tracker, {
TimeSpendReportValue: '' as IntlString, TimeSpendReportValue: '' as IntlString,
TimeSpendReportDescription: '' as IntlString, TimeSpendReportDescription: '' as IntlString,
TimeSpendValue: '' as IntlString, TimeSpendValue: '' as IntlString,
SprintPassed: '' as IntlString SprintPassed: '' as IntlString,
ChildEstimation: '' as IntlString,
ChildReportedTime: '' as IntlString
}, },
component: { component: {
NopeComponent: '' as AnyComponent, NopeComponent: '' as AnyComponent,

View File

@ -160,10 +160,13 @@ export interface Issue extends AttachedDoc {
// Estimation in man days // Estimation in man days
estimation: number estimation: number
// ReportedTime time, auto updated using trigger. // ReportedTime time, auto updated using trigger.
reportedTime: number reportedTime: number
// Collection of reportedTime entries, for proper time estimations per person. // Collection of reportedTime entries, for proper time estimations per person.
reports: number reports: number
childInfo: IssueChildInfo[]
} }
/** /**
@ -191,6 +194,15 @@ export interface IssueParentInfo {
parentTitle: string parentTitle: string
} }
/**
* @public
*/
export interface IssueChildInfo {
childId: Ref<Issue>
estimation: number
reportedTime: number
}
/** /**
* @public * @public
*/ */

View File

@ -21,10 +21,11 @@ import core, {
TxCreateDoc, TxCreateDoc,
TxCUD, TxCUD,
TxProcessor, TxProcessor,
TxUpdateDoc TxUpdateDoc,
WithLookup
} from '@anticrm/core' } from '@anticrm/core'
import { TriggerControl } from '@anticrm/server-core' import { TriggerControl } from '@anticrm/server-core'
import tracker, { Issue, TimeSpendReport } from '@anticrm/tracker' import tracker, { Issue, IssueParentInfo, TimeSpendReport } from '@anticrm/tracker'
async function updateSubIssues ( async function updateSubIssues (
updateTx: TxUpdateDoc<Issue>, updateTx: TxUpdateDoc<Issue>,
@ -57,14 +58,33 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
} }
} }
if (actualTx._class !== core.class.TxUpdateDoc) { if (actualTx._class === core.class.TxCreateDoc) {
return [] const createTx = actualTx as TxCreateDoc<Issue>
if (control.hierarchy.isDerived(createTx.objectClass, tracker.class.Issue)) {
const issue = TxProcessor.createDoc2Doc(createTx)
const res: Tx[] = []
for (const pinfo of issue.parents) {
res.push(
control.txFactory.createTxUpdateDoc(tracker.class.Issue, issue.space, pinfo.parentId, {
$push: {
childInfo: {
childId: issue._id,
estimation: issue.estimation,
reportedTime: issue.reportedTime
}
}
})
)
}
}
} }
if (actualTx._class === core.class.TxUpdateDoc) {
const updateTx = actualTx as TxUpdateDoc<Issue> const updateTx = actualTx as TxUpdateDoc<Issue>
if (control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) { if (control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) {
return await doIssueUpdate(updateTx, control) return await doIssueUpdate(updateTx, control)
} }
}
return [] return []
} }
@ -80,11 +100,15 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
switch (cud._class) { switch (cud._class) {
case core.class.TxCreateDoc: { case core.class.TxCreateDoc: {
const ccud = cud as TxCreateDoc<TimeSpendReport> const ccud = cud as TxCreateDoc<TimeSpendReport>
return [ const res = [
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, { control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
$inc: { reportedTime: ccud.attributes.value } $inc: { reportedTime: ccud.attributes.value }
}) })
] ]
const [currentIssue] = await control.findAll(tracker.class.Issue, { _id: parentTx.objectId }, { limit: 1 })
currentIssue.reportedTime += ccud.attributes.value
updateIssueParentEstimations(currentIssue, res, control, currentIssue.parents, currentIssue.parents)
return res
} }
case core.class.TxUpdateDoc: { case core.class.TxUpdateDoc: {
const upd = cud as TxUpdateDoc<TimeSpendReport> const upd = cud as TxUpdateDoc<TimeSpendReport>
@ -96,16 +120,21 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
}) })
).map(TxProcessor.extractTx) ).map(TxProcessor.extractTx)
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes) const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
const res: Tx[] = []
const [currentIssue] = await control.findAll(tracker.class.Issue, { _id: parentTx.objectId }, { limit: 1 })
if (doc !== undefined) { if (doc !== undefined) {
return [ res.push(
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, { control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
$inc: { reportedTime: -1 * doc.value } $inc: { reportedTime: upd.operations.value - doc.value }
}),
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
$inc: { reportedTime: upd.operations.value }
}) })
] )
currentIssue.reportedTime -= doc.value
currentIssue.reportedTime += upd.operations.value
} }
updateIssueParentEstimations(currentIssue, res, control, currentIssue.parents, currentIssue.parents)
return res
} }
break break
} }
@ -118,11 +147,15 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
).map(TxProcessor.extractTx) ).map(TxProcessor.extractTx)
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes) const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
if (doc !== undefined) { if (doc !== undefined) {
return [ const res = [
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, { control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
$inc: { reportedTime: -1 * doc.value } $inc: { reportedTime: -1 * doc.value }
}) })
] ]
const [currentIssue] = await control.findAll(tracker.class.Issue, { _id: parentTx.objectId }, { limit: 1 })
currentIssue.reportedTime -= doc.value
updateIssueParentEstimations(currentIssue, res, control, currentIssue.parents, currentIssue.parents)
return res
} }
} }
} }
@ -132,13 +165,26 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerControl): Promise<Tx[]> { async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = [] const res: Tx[] = []
let currentIssue: WithLookup<Issue> | undefined
async function getCurrentIssue (): Promise<WithLookup<Issue>> {
if (currentIssue !== undefined) {
return currentIssue
}
// We need to remove estimation information from out parent issue
;[currentIssue] = await control.findAll(tracker.class.Issue, { _id: updateTx.objectId }, { limit: 1 })
return currentIssue
}
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'attachedTo')) { if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'attachedTo')) {
const [newParent] = await control.findAll( const [newParent] = await control.findAll(
tracker.class.Issue, tracker.class.Issue,
{ _id: updateTx.operations.attachedTo as Ref<Issue> }, { _id: updateTx.operations.attachedTo as Ref<Issue> },
{ limit: 1 } { limit: 1 }
) )
const updatedProject = newParent !== undefined ? newParent.project : null const updatedProject = newParent !== undefined ? newParent.project : null
const updatedSprint = newParent !== undefined ? newParent.sprint : null
const updatedParents = const updatedParents =
newParent !== undefined ? [{ parentId: newParent._id, parentTitle: newParent.title }, ...newParent.parents] : [] newParent !== undefined ? [{ parentId: newParent._id, parentTitle: newParent.title }, ...newParent.parents] : []
@ -149,20 +195,44 @@ async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerCont
? {} ? {}
: { parents: [...issue.parents].slice(0, parentInfoIndex + 1).concat(updatedParents) } : { parents: [...issue.parents].slice(0, parentInfoIndex + 1).concat(updatedParents) }
return { ...parentsUpdate, project: updatedProject } return { ...parentsUpdate, project: updatedProject, sprint: updatedSprint }
} }
res.push( res.push(
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, { control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
parents: updatedParents, parents: updatedParents,
project: updatedProject project: updatedProject,
sprint: updatedSprint
}), }),
...(await updateSubIssues(updateTx, control, update)) ...(await updateSubIssues(updateTx, control, update))
) )
// Remove from parent estimation list.
const issue = await getCurrentIssue()
updateIssueParentEstimations(issue, res, control, issue.parents, updatedParents)
} }
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'project')) { if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'project')) {
res.push(...(await updateSubIssues(updateTx, control, { project: updateTx.operations.project }))) res.push(
...(await updateSubIssues(updateTx, control, {
project: updateTx.operations.project,
sprint: updateTx.operations.sprint
}))
)
}
if (
Object.prototype.hasOwnProperty.call(updateTx.operations, 'estimation') ||
Object.prototype.hasOwnProperty.call(updateTx.operations, 'reportedTime')
) {
const issue = await getCurrentIssue()
issue.estimation = updateTx.operations.estimation ?? issue.estimation
issue.reportedTime = updateTx.operations.reportedTime ?? issue.reportedTime
updateIssueParentEstimations(issue, res, control, issue.parents, issue.parents)
}
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'sprint')) {
res.push(...(await updateSubIssues(updateTx, control, { sprint: updateTx.operations.sprint })))
} }
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'title')) { if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'title')) {
@ -181,3 +251,33 @@ async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerCont
return res return res
} }
function updateIssueParentEstimations (
issue: WithLookup<Issue>,
res: Tx[],
control: TriggerControl,
sourceParents: IssueParentInfo[],
targetParents: IssueParentInfo[]
): void {
for (const pinfo of sourceParents) {
res.push(
control.txFactory.createTxUpdateDoc(tracker.class.Issue, issue.space, pinfo.parentId, {
$pull: {
childInfo: { childId: issue._id }
}
})
)
}
for (const pinfo of targetParents) {
res.push(
control.txFactory.createTxUpdateDoc(tracker.class.Issue, issue.space, pinfo.parentId, {
$push: {
childInfo: {
childId: issue._id,
estimation: issue.estimation,
reportedTime: issue.reportedTime
}
}
})
)
}
}

View File

@ -178,9 +178,10 @@ abstract class MongoAdapterBase extends TxProcessor {
parent?: string parent?: string
): Promise<any | undefined> { ): Promise<any | undefined> {
const fullKey = parent !== undefined ? parent + '.' + '_id' : '_id' const fullKey = parent !== undefined ? parent + '.' + '_id' : '_id'
for (const key in lookup._id) { const lid = lookup?._id ?? {}
for (const key in lid) {
const as = parent !== undefined ? parent + key : key const as = parent !== undefined ? parent + key : key
const value = lookup._id[key] const value = lid[key]
let _class: Ref<Class<Doc>> let _class: Ref<Class<Doc>>
let attr = 'attachedTo' let attr = 'attachedTo'