mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
Subissue estimations (#2254)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
854331ae5f
commit
ccd2048ad0
@ -45,7 +45,7 @@ import attachment from '@anticrm/model-attachment'
|
||||
import chunter from '@anticrm/model-chunter'
|
||||
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
|
||||
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 type { Asset, IntlString } from '@anticrm/platform'
|
||||
import setting from '@anticrm/setting'
|
||||
@ -58,6 +58,7 @@ export const DOMAIN_CHANNEL = 'channel' as Domain
|
||||
export class TChannelProvider extends TDoc implements ChannelProvider {
|
||||
label!: IntlString
|
||||
icon?: Asset
|
||||
action?: ViewAction
|
||||
placeholder!: IntlString
|
||||
}
|
||||
|
||||
@ -305,7 +306,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
label: contact.string.LinkedIn,
|
||||
icon: contact.icon.LinkedIn,
|
||||
placeholder: contact.string.LinkedInPlaceholder
|
||||
placeholder: contact.string.LinkedInPlaceholder,
|
||||
action: contact.actionImpl.OpenChannel
|
||||
},
|
||||
contact.channelProvider.LinkedIn
|
||||
)
|
||||
@ -316,7 +318,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
label: contact.string.Twitter,
|
||||
icon: contact.icon.Twitter,
|
||||
placeholder: contact.string.AtPlaceHolder
|
||||
placeholder: contact.string.AtPlaceHolder,
|
||||
action: contact.actionImpl.OpenChannel
|
||||
},
|
||||
contact.channelProvider.Twitter
|
||||
)
|
||||
@ -327,7 +330,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
label: contact.string.GitHub,
|
||||
icon: contact.icon.GitHub,
|
||||
placeholder: contact.string.AtPlaceHolder
|
||||
placeholder: contact.string.AtPlaceHolder,
|
||||
action: contact.actionImpl.OpenChannel
|
||||
},
|
||||
contact.channelProvider.GitHub
|
||||
)
|
||||
@ -338,7 +342,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
label: contact.string.Facebook,
|
||||
icon: contact.icon.Facebook,
|
||||
placeholder: contact.string.FacebookPlaceholder
|
||||
placeholder: contact.string.FacebookPlaceholder,
|
||||
action: contact.actionImpl.OpenChannel
|
||||
},
|
||||
contact.channelProvider.Facebook
|
||||
)
|
||||
@ -349,7 +354,8 @@ export function createModel (builder: Builder): void {
|
||||
{
|
||||
label: contact.string.Homepage,
|
||||
icon: contact.icon.Homepage,
|
||||
placeholder: contact.string.HomepagePlaceholder
|
||||
placeholder: contact.string.HomepagePlaceholder,
|
||||
action: contact.actionImpl.OpenChannel
|
||||
},
|
||||
contact.channelProvider.Homepage
|
||||
)
|
||||
|
@ -83,6 +83,7 @@ export default mergeIds(contactId, contact, {
|
||||
KickEmployee: '' as Ref<Action>
|
||||
},
|
||||
actionImpl: {
|
||||
KickEmployee: '' as ViewAction
|
||||
KickEmployee: '' as ViewAction,
|
||||
OpenChannel: '' as ViewAction
|
||||
}
|
||||
})
|
||||
|
@ -307,6 +307,7 @@ export function createModel (builder: Builder): void {
|
||||
label: recruit.string.Applications
|
||||
},
|
||||
'$lookup.company',
|
||||
'$lookup.company.$lookup.channels',
|
||||
'location',
|
||||
'description',
|
||||
{
|
||||
|
@ -55,7 +55,8 @@ import {
|
||||
SprintStatus,
|
||||
Team,
|
||||
TimeSpendReport,
|
||||
trackerId
|
||||
trackerId,
|
||||
IssueChildInfo
|
||||
} from '@anticrm/tracker'
|
||||
import { KeyBinding } from '@anticrm/view'
|
||||
import tracker from './plugin'
|
||||
@ -238,6 +239,8 @@ export class TIssue extends TAttachedDoc implements Issue {
|
||||
|
||||
@Prop(Collection(tracker.class.TimeSpendReport), tracker.string.TimeSpendReports)
|
||||
reports!: number
|
||||
|
||||
declare childInfo: IssueChildInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,7 +32,11 @@ function $push (document: Doc, keyval: Record<string, PropertyType>): void {
|
||||
if (typeof val === 'object') {
|
||||
const arr = doc[key] as Array<any>
|
||||
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 {
|
||||
doc[key].push(val)
|
||||
}
|
||||
@ -48,7 +52,20 @@ function $pull (document: Doc, keyval: Record<string, PropertyType>): void {
|
||||
const arr = doc[key] as Array<any>
|
||||
if (typeof keyval[key] === 'object') {
|
||||
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 {
|
||||
doc[key] = arr.filter((val) => val !== keyval[key])
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ export interface TxMixin<D extends Doc, M extends D> extends TxCUD<D> {
|
||||
* @public
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -596,10 +596,12 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
;(result as any)[key] = objects[0]
|
||||
const nestedResult = {}
|
||||
const parent = (result as any)[key]
|
||||
if (parent !== undefined) {
|
||||
await this.getLookupValue(_class, parent, nested, nestedResult)
|
||||
Object.assign(parent, {
|
||||
$lookup: nestedResult
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const objects = await this.findAll(value, { _id: getObjectValue(tkey, doc) })
|
||||
;(result as any)[key] = objects[0]
|
||||
|
@ -22,9 +22,9 @@
|
||||
</script>
|
||||
|
||||
{#if attachments.length}
|
||||
<div class="flex-col">
|
||||
<div class="flex flex-wrap">
|
||||
{#each attachments as attachment}
|
||||
<div class="step-tb75">
|
||||
<div class="p-2">
|
||||
<AttachmentPreview value={attachment} isSaved={savedAttachmentsIds?.includes(attachment._id) ?? false} />
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -13,23 +13,22 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, afterUpdate } from 'svelte'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import type { PopupOptions } from '@anticrm/ui'
|
||||
import {
|
||||
Button,
|
||||
IconClose,
|
||||
closeTooltip,
|
||||
IconBlueCheck,
|
||||
registerFocus,
|
||||
createFocusManager,
|
||||
FocusHandler,
|
||||
IconArrowRight,
|
||||
IconEdit
|
||||
IconBlueCheck,
|
||||
IconClose,
|
||||
IconEdit,
|
||||
registerFocus
|
||||
} from '@anticrm/ui'
|
||||
import IconCopy from './icons/Copy.svelte'
|
||||
import { FocusHandler } from '@anticrm/ui'
|
||||
import type { PopupOptions } from '@anticrm/ui'
|
||||
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
|
||||
import plugin from '../plugin'
|
||||
import IconCopy from './icons/Copy.svelte'
|
||||
|
||||
export let value: string = ''
|
||||
export let placeholder: IntlString
|
||||
@ -101,8 +100,9 @@
|
||||
style="width: 100%;"
|
||||
on:keypress={(ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
dispatch('close', value)
|
||||
closeTooltip()
|
||||
}
|
||||
}}
|
||||
on:change
|
||||
|
@ -32,6 +32,7 @@
|
||||
Menu,
|
||||
showPopup
|
||||
} from '@anticrm/ui'
|
||||
import { ViewAction } from '@anticrm/view'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import { getChannelProviders } from '../utils'
|
||||
import ChannelEditor from './ChannelEditor.svelte'
|
||||
@ -55,6 +56,7 @@
|
||||
icon: Asset
|
||||
value: string
|
||||
presenter?: AnyComponent
|
||||
action?: ViewAction
|
||||
placeholder: IntlString
|
||||
provider: Ref<ChannelProvider>
|
||||
integration: boolean
|
||||
@ -74,6 +76,7 @@
|
||||
icon: provider.icon as Asset,
|
||||
value: item.value,
|
||||
presenter: provider.presenter,
|
||||
action: provider.action,
|
||||
placeholder: provider.placeholder,
|
||||
provider: provider._id,
|
||||
notification,
|
||||
|
@ -16,8 +16,10 @@
|
||||
<script lang="ts">
|
||||
import type { Channel } from '@anticrm/contact'
|
||||
import { Doc } from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import { showPopup } from '@anticrm/ui'
|
||||
import { ViewAction } from '@anticrm/view'
|
||||
import ChannelsDropdown from './ChannelsDropdown.svelte'
|
||||
|
||||
export let value: Channel[] | Channel | null
|
||||
@ -29,12 +31,16 @@
|
||||
export let shape: 'circle' | undefined = 'circle'
|
||||
export let object: Doc
|
||||
|
||||
function _open (ev: any) {
|
||||
async function _open (ev: CustomEvent): Promise<void> {
|
||||
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')
|
||||
}
|
||||
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>
|
||||
|
@ -14,41 +14,41 @@
|
||||
// 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 { leaveWorkspace } from '@anticrm/login-resources'
|
||||
import { Resources } from '@anticrm/platform'
|
||||
import { Avatar, getClient, MessageBox, ObjectSearchResult, UserInfo } from '@anticrm/presentation'
|
||||
import { showPopup } from '@anticrm/ui'
|
||||
import Channels from './components/Channels.svelte'
|
||||
import ChannelsDropdown from './components/ChannelsDropdown.svelte'
|
||||
import ChannelsEditor from './components/ChannelsEditor.svelte'
|
||||
import ChannelsPresenter from './components/ChannelsPresenter.svelte'
|
||||
import ChannelsView from './components/ChannelsView.svelte'
|
||||
import ChannelsDropdown from './components/ChannelsDropdown.svelte'
|
||||
import ContactPresenter from './components/ContactPresenter.svelte'
|
||||
import Contacts from './components/Contacts.svelte'
|
||||
import CreateEmployee from './components/CreateEmployee.svelte'
|
||||
import CreateOrganization from './components/CreateOrganization.svelte'
|
||||
import CreateOrganizations from './components/CreateOrganizations.svelte'
|
||||
import CreatePerson from './components/CreatePerson.svelte'
|
||||
import CreatePersons from './components/CreatePersons.svelte'
|
||||
import EditMember from './components/EditMember.svelte'
|
||||
import EditOrganization from './components/EditOrganization.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 OrganizationSelector from './components/OrganizationSelector.svelte'
|
||||
import PersonEditor from './components/PersonEditor.svelte'
|
||||
import PersonPresenter from './components/PersonPresenter.svelte'
|
||||
import SocialEditor from './components/SocialEditor.svelte'
|
||||
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 {
|
||||
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> => ({
|
||||
actionImpl: {
|
||||
KickEmployee: kickEmployee
|
||||
KickEmployee: kickEmployee,
|
||||
OpenChannel: openChannelURL
|
||||
},
|
||||
component: {
|
||||
PersonEditor,
|
||||
|
@ -30,7 +30,7 @@ import {
|
||||
import type { Asset, Plugin } from '@anticrm/platform'
|
||||
import { IntlString, plugin } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import { Viewlet } from '@anticrm/view'
|
||||
import { ViewAction, Viewlet } from '@anticrm/view'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -46,8 +46,16 @@ export interface Persons extends Space {}
|
||||
* @public
|
||||
*/
|
||||
export interface ChannelProvider extends Doc, UXObject {
|
||||
// Placeholder
|
||||
placeholder: IntlString
|
||||
|
||||
// Presenter will be shown on click for channel
|
||||
presenter?: AnyComponent
|
||||
|
||||
// Action to be performed if there is no presenter defined.
|
||||
action?: ViewAction
|
||||
|
||||
// Integration type
|
||||
integrationType?: Ref<Doc>
|
||||
}
|
||||
|
||||
|
@ -208,7 +208,9 @@
|
||||
"TimeSpendReportValueTooltip": "Reported time in man days",
|
||||
"TimeSpendReportDescription": "Description",
|
||||
"TimeSpendValue": "{value}d",
|
||||
"SprintPassed": "{from}d/{to}d"
|
||||
"SprintPassed": "{from}d/{to}d",
|
||||
"ChildEstimation": "Subissues Estimation",
|
||||
"ChildReportedTime": "Subissues Time"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -208,7 +208,9 @@
|
||||
"TimeSpendReportValueTooltip": "Затраченное время в человеко днях",
|
||||
"TimeSpendReportDescription": "Описание",
|
||||
"TimeSpendValue": "{value}d",
|
||||
"SprintPassed": "{from}d/{to}d"
|
||||
"SprintPassed": "{from}d/{to}d",
|
||||
"ChildEstimation": "Subissues Estimation",
|
||||
"ChildReportedTime": "Subissues Time"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@
|
||||
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
|
||||
import SprintSelector from './sprints/SprintSelector.svelte'
|
||||
import { activeProject, activeSprint } from '../issues'
|
||||
import EstimationEditor from './issues/timereport/EstimationEditor.svelte'
|
||||
|
||||
export let space: Ref<Team>
|
||||
export let status: Ref<IssueStatus> | undefined = undefined
|
||||
@ -71,7 +72,8 @@
|
||||
parents: [],
|
||||
reportedTime: 0,
|
||||
estimation: 0,
|
||||
reports: 0
|
||||
reports: 0,
|
||||
childInfo: []
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -156,8 +158,9 @@
|
||||
? [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
|
||||
: [],
|
||||
reportedTime: 0,
|
||||
estimation: 0,
|
||||
reports: 0
|
||||
estimation: object.estimation,
|
||||
reports: 0,
|
||||
childInfo: []
|
||||
}
|
||||
|
||||
await client.addCollection(
|
||||
@ -344,6 +347,7 @@
|
||||
labels = labels.filter((it) => it._id !== evt.detail)
|
||||
}}
|
||||
/>
|
||||
<EstimationEditor kind={'no-border'} size={'small'} value={object} />
|
||||
<ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} />
|
||||
<SprintSelector value={object.sprint} onSprintIdChange={handleSprintIdChanged} />
|
||||
{#if object.dueDate !== null}
|
||||
|
@ -21,8 +21,12 @@
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
// export let inline: boolean = false
|
||||
export let disableClick = false
|
||||
|
||||
function handleIssueEditorOpened () {
|
||||
if (disableClick) {
|
||||
return
|
||||
}
|
||||
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
|
||||
}
|
||||
|
||||
@ -39,7 +43,7 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<span class="issuePresenterRoot" title="title" on:click={handleIssueEditorOpened}>
|
||||
<span class="issuePresenterRoot" class:noPointer={disableClick} title="title" on:click={handleIssueEditorOpened}>
|
||||
{title}
|
||||
</span>
|
||||
{/if}
|
||||
@ -56,6 +60,10 @@
|
||||
color: var(--content-color);
|
||||
cursor: pointer;
|
||||
|
||||
&.noPointer {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
text-decoration: underline;
|
||||
|
@ -24,6 +24,7 @@
|
||||
import AssigneeEditor from '../AssigneeEditor.svelte'
|
||||
import StatusEditor from '../StatusEditor.svelte'
|
||||
import PriorityEditor from '../PriorityEditor.svelte'
|
||||
import EstimationEditor from '../timereport/EstimationEditor.svelte'
|
||||
|
||||
export let parentIssue: Issue
|
||||
export let issueStatuses: WithLookup<IssueStatus>[]
|
||||
@ -55,7 +56,12 @@
|
||||
dueDate: null,
|
||||
comments: 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)
|
||||
}}
|
||||
/>
|
||||
<EstimationEditor kind={'no-border'} size={'small'} value={newIssue} />
|
||||
</div>
|
||||
<div class="buttons-group small-gap">
|
||||
<Button label={presentation.string.Cancel} size="small" kind="transparent" on:click={close} />
|
||||
|
@ -279,7 +279,9 @@
|
||||
<SubIssues {issue} {issueStatuses} {currentTeam} />
|
||||
{/key}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<AttachmentDocList value={issue} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span slot="actions-label">
|
||||
|
@ -32,6 +32,7 @@
|
||||
import DueDateEditor from '../DueDateEditor.svelte'
|
||||
import PriorityEditor from '../PriorityEditor.svelte'
|
||||
import StatusEditor from '../StatusEditor.svelte'
|
||||
import EstimationEditor from '../timereport/EstimationEditor.svelte'
|
||||
|
||||
export let issues: Issue[]
|
||||
export let issueStatuses: WithLookup<IssueStatus>[]
|
||||
@ -148,6 +149,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-center flex-no-shrink">
|
||||
<EstimationEditor value={issue} kind={'list'} />
|
||||
{#if issue.dueDate !== null}
|
||||
<DueDateEditor value={issue} />
|
||||
{/if}
|
||||
|
@ -13,15 +13,18 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachedData } from '@anticrm/core'
|
||||
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Issue } from '@anticrm/tracker'
|
||||
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 tracker from '../../../plugin'
|
||||
import EstimationPopup from './EstimationPopup.svelte'
|
||||
import EstimationProgressCircle from './EstimationProgressCircle.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let value: Issue | AttachedData<Issue>
|
||||
export let isEditable: boolean = true
|
||||
|
||||
export let kind: ButtonKind = 'link'
|
||||
@ -39,6 +42,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (kind === 'list') {
|
||||
showPopup(
|
||||
EstimationPopup,
|
||||
{ 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) => {
|
||||
@ -60,22 +71,67 @@
|
||||
|
||||
if ('_id' in value) {
|
||||
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>
|
||||
|
||||
{#if value}
|
||||
{#if kind === 'list'}
|
||||
<div class="estimation-container" on:click={handleestimationEditorOpened}>
|
||||
<div class="icon">
|
||||
<EstimationProgressCircle value={value.reportedTime} max={value.estimation} />
|
||||
<EstimationProgressCircle value={Math.max(value.reportedTime, childReportTime)} max={value.estimation} />
|
||||
</div>
|
||||
<span class="overflow-label label">
|
||||
{#if value.reportedTime > 0}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: value.reportedTime }} />
|
||||
/
|
||||
<span class="overflow-label label flex-row-center flex-nowrap text-md">
|
||||
{#if value.reportedTime > 0 || childReportTime > 0}
|
||||
{#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}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: value.estimation }} />
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
@ -122,5 +178,15 @@
|
||||
color: var(--caption-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.showError {
|
||||
color: var(--error-color) !important;
|
||||
}
|
||||
.showWarning {
|
||||
color: var(--warning-color) !important;
|
||||
}
|
||||
.romColor {
|
||||
color: var(--content-color) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -16,14 +16,15 @@
|
||||
<script lang="ts">
|
||||
import contact from '@anticrm/contact'
|
||||
import { FindOptions } from '@anticrm/core'
|
||||
import { Card } from '@anticrm/presentation'
|
||||
import presentation, { Card } from '@anticrm/presentation'
|
||||
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 { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../../plugin'
|
||||
import IssuePresenter from '../IssuePresenter.svelte'
|
||||
import ParentNamesPresenter from '../ParentNamesPresenter.svelte'
|
||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||
import presentation from '@anticrm/presentation'
|
||||
|
||||
export let value: string | number | undefined
|
||||
export let format: 'text' | 'password' | 'number'
|
||||
@ -39,8 +40,9 @@
|
||||
if (ev.key === 'Enter') dispatch('close', _value)
|
||||
}
|
||||
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>
|
||||
|
||||
<Card
|
||||
@ -54,6 +56,9 @@
|
||||
dispatch('close', null)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<IssuePresenter value={object} disableClick />
|
||||
</svelte:fragment>
|
||||
<div class="header no-border flex-col p-1">
|
||||
<div class="flex-row-center flex-between">
|
||||
<EditBox
|
||||
@ -67,11 +72,28 @@
|
||||
/>
|
||||
</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>
|
||||
<TableBrowser
|
||||
_class={tracker.class.TimeSpendReport}
|
||||
query={{ attachedTo: object._id }}
|
||||
config={['', '$lookup.employee', 'date', 'description']}
|
||||
query={{ attachedTo: { $in: [object._id, ...childIds] } }}
|
||||
config={[
|
||||
'$lookup.attachedTo',
|
||||
{ key: '$lookup.attachedTo', presenter: ParentNamesPresenter },
|
||||
'',
|
||||
'$lookup.employee',
|
||||
'date',
|
||||
'description'
|
||||
]}
|
||||
{options}
|
||||
/>
|
||||
</Scroller>
|
||||
|
@ -25,9 +25,6 @@
|
||||
export let greenColor: string = FernColor
|
||||
export let overdueColor = FlamingoColor
|
||||
|
||||
if (value > max) value = max
|
||||
if (value < min) value = min
|
||||
|
||||
const lenghtC: number = Math.PI * 14 - 1
|
||||
$: procC = lenghtC / (max - min)
|
||||
$: dashOffset = (Math.min(value, max) - min) * procC
|
||||
|
@ -36,12 +36,18 @@
|
||||
function showReports (event: MouseEvent): void {
|
||||
showPopup(ReportsPopup, { issue: object }, eventToHTMLElement(event))
|
||||
}
|
||||
$: childTime = (object.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0)
|
||||
</script>
|
||||
|
||||
{#if kind === 'link'}
|
||||
<div class="link-container flex-between" on:click={showReports}>
|
||||
{#if value !== undefined}
|
||||
<span class="overflow-label">{value}</span>
|
||||
<span class="overflow-label">
|
||||
{value}
|
||||
{#if childTime !== 0}
|
||||
/ {childTime}
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
@ -50,7 +56,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else if value !== undefined}
|
||||
<span class="overflow-label">{value}</span>
|
||||
<span class="overflow-label">
|
||||
{value}
|
||||
{#if childTime !== 0}
|
||||
/ {childTime}
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
|
@ -20,6 +20,7 @@
|
||||
import { Button, eventToHTMLElement, IconAdd, Scroller, showPopup } from '@anticrm/ui'
|
||||
import { TableBrowser } from '@anticrm/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import IssuePresenter from '../IssuePresenter.svelte'
|
||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||
export let issue: Issue
|
||||
|
||||
@ -27,7 +28,10 @@
|
||||
return true
|
||||
}
|
||||
const options: FindOptions<TimeSpendReport> = {
|
||||
lookup: { employee: contact.class.Employee }
|
||||
lookup: {
|
||||
attachedTo: tracker.class.Issue,
|
||||
employee: contact.class.Employee
|
||||
}
|
||||
}
|
||||
function addReport (event: MouseEvent): void {
|
||||
showPopup(
|
||||
@ -45,11 +49,23 @@
|
||||
okAction={() => {}}
|
||||
okLabel={presentation.string.Ok}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<IssuePresenter value={issue} disableClick />
|
||||
</svelte:fragment>
|
||||
<Scroller tableFade>
|
||||
<TableBrowser
|
||||
_class={tracker.class.TimeSpendReport}
|
||||
query={{ attachedTo: issue._id }}
|
||||
config={['', '$lookup.employee', 'description', 'date', 'modifiedOn', 'modifiedBy']}
|
||||
query={{ attachedTo: { $in: [issue._id, ...issue.childInfo.map((it) => it.childId)] } }}
|
||||
config={[
|
||||
'$lookup.attachedTo',
|
||||
'$lookup.attachedTo.title',
|
||||
'',
|
||||
'$lookup.employee',
|
||||
'description',
|
||||
'date',
|
||||
'modifiedOn',
|
||||
'modifiedBy'
|
||||
]}
|
||||
{options}
|
||||
/>
|
||||
</Scroller>
|
||||
|
@ -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)
|
||||
.reduce((it, cur) => {
|
||||
return it + cur
|
||||
})
|
||||
$: totalReported = (issues ?? [{ reportedTime: 0 }])
|
||||
$: totalReported = (noParents ?? [{ reportedTime: 0 }])
|
||||
.map((it) => it.reportedTime)
|
||||
.reduce((it, cur) => {
|
||||
return it + cur
|
||||
@ -108,6 +111,7 @@
|
||||
{@const now = Date.now()}
|
||||
{#if sprint.startDate < now && now < sprint.targetDate}
|
||||
<!-- Active sprint in time -->
|
||||
<div class="ml-2">
|
||||
<Label
|
||||
label={tracker.string.SprintPassed}
|
||||
params={{
|
||||
@ -115,6 +119,7 @@
|
||||
to: getDayOfSprint(sprint.startDate, sprint.targetDate) - 1
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
|
||||
|
@ -224,7 +224,10 @@ export default mergeIds(trackerId, tracker, {
|
||||
TimeSpendReportValue: '' as IntlString,
|
||||
TimeSpendReportDescription: '' as IntlString,
|
||||
TimeSpendValue: '' as IntlString,
|
||||
SprintPassed: '' as IntlString
|
||||
SprintPassed: '' as IntlString,
|
||||
|
||||
ChildEstimation: '' as IntlString,
|
||||
ChildReportedTime: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
NopeComponent: '' as AnyComponent,
|
||||
|
@ -160,10 +160,13 @@ export interface Issue extends AttachedDoc {
|
||||
|
||||
// Estimation in man days
|
||||
estimation: number
|
||||
|
||||
// ReportedTime time, auto updated using trigger.
|
||||
reportedTime: number
|
||||
// Collection of reportedTime entries, for proper time estimations per person.
|
||||
reports: number
|
||||
|
||||
childInfo: IssueChildInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -191,6 +194,15 @@ export interface IssueParentInfo {
|
||||
parentTitle: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IssueChildInfo {
|
||||
childId: Ref<Issue>
|
||||
estimation: number
|
||||
reportedTime: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -21,10 +21,11 @@ import core, {
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
TxUpdateDoc,
|
||||
WithLookup
|
||||
} from '@anticrm/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 (
|
||||
updateTx: TxUpdateDoc<Issue>,
|
||||
@ -57,14 +58,33 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
|
||||
}
|
||||
}
|
||||
|
||||
if (actualTx._class !== core.class.TxUpdateDoc) {
|
||||
return []
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
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>
|
||||
if (control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) {
|
||||
return await doIssueUpdate(updateTx, control)
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
@ -80,11 +100,15 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
|
||||
switch (cud._class) {
|
||||
case core.class.TxCreateDoc: {
|
||||
const ccud = cud as TxCreateDoc<TimeSpendReport>
|
||||
return [
|
||||
const res = [
|
||||
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
|
||||
$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: {
|
||||
const upd = cud as TxUpdateDoc<TimeSpendReport>
|
||||
@ -96,16 +120,21 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
|
||||
})
|
||||
).map(TxProcessor.extractTx)
|
||||
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) {
|
||||
return [
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
|
||||
$inc: { reportedTime: -1 * doc.value }
|
||||
}),
|
||||
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
|
||||
$inc: { reportedTime: upd.operations.value }
|
||||
$inc: { reportedTime: upd.operations.value - doc.value }
|
||||
})
|
||||
]
|
||||
)
|
||||
currentIssue.reportedTime -= doc.value
|
||||
currentIssue.reportedTime += upd.operations.value
|
||||
}
|
||||
|
||||
updateIssueParentEstimations(currentIssue, res, control, currentIssue.parents, currentIssue.parents)
|
||||
return res
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -118,11 +147,15 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
|
||||
).map(TxProcessor.extractTx)
|
||||
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
|
||||
if (doc !== undefined) {
|
||||
return [
|
||||
const res = [
|
||||
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
|
||||
$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[]> {
|
||||
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')) {
|
||||
const [newParent] = await control.findAll(
|
||||
tracker.class.Issue,
|
||||
{ _id: updateTx.operations.attachedTo as Ref<Issue> },
|
||||
{ limit: 1 }
|
||||
)
|
||||
|
||||
const updatedProject = newParent !== undefined ? newParent.project : null
|
||||
const updatedSprint = newParent !== undefined ? newParent.sprint : null
|
||||
const updatedParents =
|
||||
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) }
|
||||
|
||||
return { ...parentsUpdate, project: updatedProject }
|
||||
return { ...parentsUpdate, project: updatedProject, sprint: updatedSprint }
|
||||
}
|
||||
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
|
||||
parents: updatedParents,
|
||||
project: updatedProject
|
||||
project: updatedProject,
|
||||
sprint: updatedSprint
|
||||
}),
|
||||
...(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')) {
|
||||
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')) {
|
||||
@ -181,3 +251,33 @@ async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerCont
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -178,9 +178,10 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
parent?: string
|
||||
): Promise<any | undefined> {
|
||||
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 value = lookup._id[key]
|
||||
const value = lid[key]
|
||||
|
||||
let _class: Ref<Class<Doc>>
|
||||
let attr = 'attachedTo'
|
||||
|
Loading…
Reference in New Issue
Block a user