Tracker fixes (#2255)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-08-23 01:55:45 +07:00 committed by GitHub
parent ccd2048ad0
commit f1fad745bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 202 additions and 102 deletions

View File

@ -1,10 +0,0 @@
{
"changes": [
{
"packageName": "@anticrm/workspace",
"comment": "",
"type": "none"
}
],
"packageName": "@anticrm/workspace"
}

View File

@ -394,6 +394,8 @@ export function createModel (builder: Builder): void {
props: { kind: 'list', size: 'small', justify: 'center' }
},
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true, fixed: 'left' } },
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
{ key: '', presenter: tracker.component.GrowPresenter, props: {} },
{ key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
{
key: '',

View File

@ -38,7 +38,8 @@ export default mergeIds(trackerId, tracker, {
component: {
// Required to pass build without errorsF
Nope: '' as AnyComponent,
SprintSelector: '' as AnyComponent
SprintSelector: '' as AnyComponent,
SubIssuesSelector: '' as AnyComponent
},
app: {
Tracker: '' as Ref<Application>

View File

@ -1,6 +1,6 @@
{
"name": "@anticrm/core",
"version": "0.6.16",
"version": "0.6.17",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",

View File

@ -0,0 +1,16 @@
<span class="root" />
<style lang="scss">
.root {
display: flex;
flex-grow: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
flex-shrink: 10;
&.with-margin {
margin-left: 0.5rem;
}
}
</style>

View File

@ -16,7 +16,7 @@
import contact, { Employee } from '@anticrm/contact'
import { Class, Doc, FindOptions, getObjectValue, Ref, WithLookup } from '@anticrm/core'
import notification from '@anticrm/notification'
import { getClient } from '@anticrm/presentation'
import { createQuery, getClient } from '@anticrm/presentation'
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
import {
Button,
@ -36,6 +36,7 @@
import tracker from '../../plugin'
import { IssuesGroupByKeys, issuesGroupEditorMap, IssuesOrderByKeys, issuesSortOrderMap } from '../../utils'
import CreateIssue from '../CreateIssue.svelte'
import GrowPresenter from './GrowPresenter.svelte'
export let _class: Ref<Class<Doc>>
export let currentSpace: Ref<Team> | undefined = undefined
@ -59,10 +60,19 @@
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team
space: tracker.class.Team,
_id: {
subIssues: tracker.class.Issue
}
}
}
const spaceQuery = createQuery()
let currentTeam: Team | undefined
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
currentTeam = res.shift()
})
let personPresenter: AttributeModel
$: isCollapsedMap = Object.fromEntries(categories.map((category) => [category, false]))
@ -175,6 +185,7 @@
shouldShowPlaceholder={true}
isInteractive={false}
avatarSize={'x-small'}
{currentSpace}
/>
{:else if headerComponent}
<Component
@ -186,7 +197,8 @@
statuses: groupByKey === 'status' ? statuses : undefined,
issues: groupedIssues[category],
size: 'inline',
kind: 'list'
kind: 'list',
currentSpace
}}
/>
{/if}
@ -244,6 +256,8 @@
value={getObjectValue(attributeModel.key, docObject) ?? ''}
groupBy={groupByKey}
{...attributeModel.props}
{statuses}
{currentTeam}
/>
</div>
{:else if attributeModelIndex === 1}
@ -259,10 +273,12 @@
value={getObjectValue(attributeModel.key, docObject) ?? ''}
groupBy={groupByKey}
{...attributeModel.props}
{statuses}
{currentTeam}
/>
</FixedColumn>
</div>
{:else if attributeModelIndex === 3}
{:else if attributeModelIndex === 3 || attributeModel.presenter === GrowPresenter}
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
@ -281,6 +297,8 @@
value={getObjectValue(attributeModel.key, docObject) ?? ''}
groupBy={groupByKey}
{...attributeModel.props}
{statuses}
{currentTeam}
/>
</FixedColumn>
{:else}
@ -291,6 +309,8 @@
issueId={docObject._id}
groupBy={groupByKey}
{...attributeModel.props}
{statuses}
{currentTeam}
/>
</div>
{/if}
@ -367,12 +387,7 @@
.priorityPresenter,
.issuePresenter {
min-width: 0;
// min-width: 0;
min-height: 0;
}
// .grow-cell {
// flex-grow: 1;
// flex-shrink: 0;
// min-width: 0;
// }
</style>

View File

@ -252,7 +252,7 @@
</div>
<div class="buttons-group xsmall-gap states-bar">
{#if issue && issueStatuses && issue.subIssues > 0}
<SubIssuesSelector {issue} {currentTeam} {issueStatuses} />
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses} />
{/if}
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
<ProjectEditor

View File

@ -58,7 +58,10 @@
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team,
sprint: tracker.class.Sprint
sprint: tracker.class.Sprint,
_id: {
subIssues: tracker.class.Issue
}
}
}
)

View File

@ -30,8 +30,8 @@
<style lang="scss">
.root {
display: flex;
flex-grow: 1;
min-width: 0;
flex-grow: 0;
min-width: 7rem;
white-space: nowrap;
overflow: hidden;
flex-shrink: 10;

View File

@ -21,9 +21,9 @@
import tracker from '../../../plugin'
import { getIssueId } from '../../../issues'
export let issue: WithLookup<Issue>
export let value: WithLookup<Issue>
export let currentTeam: Team | undefined
export let issueStatuses: WithLookup<IssueStatus>[] | undefined
export let statuses: WithLookup<IssueStatus>[] | undefined
export let kind: ButtonKind = 'link-bordered'
export let size: ButtonSize = 'inline'
@ -36,18 +36,18 @@
let doneStatus: Ref<Doc> | undefined
let countComplate: number = 0
$: if (issue.$lookup?.subIssues !== undefined) {
subIssues = issue.$lookup.subIssues as Issue[]
subIssues.sort((a, b) => a.rank.localeCompare(b.rank))
$: if (value.$lookup?.subIssues !== undefined) {
subIssues = value.$lookup.subIssues as Issue[]
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
}
$: if (issueStatuses && subIssues) {
doneStatus = issueStatuses.find((s) => s.category === tracker.issueStatusCategory.Completed)?._id ?? undefined
$: if (statuses && subIssues) {
doneStatus = statuses.find((s) => s.category === tracker.issueStatusCategory.Completed)?._id ?? undefined
if (doneStatus) countComplate = subIssues.filter((si) => si.status === doneStatus).length
}
$: hasSubIssues = (subIssues?.length ?? 0) > 0
function getIssueStatusIcon (issue: Issue) {
const status = issueStatuses?.find((s) => issue.status === s._id)
const status = statuses?.find((s) => issue.status === s._id)
const category = status?.$lookup?.category
const color = status?.color ?? category?.color
@ -58,8 +58,8 @@
}
function openIssue (target: Ref<Issue>) {
if (target !== issue._id) {
showPanel(tracker.component.EditIssue, target, issue._class, 'content')
if (target !== value._id) {
showPanel(tracker.component.EditIssue, target, value._class, 'content')
}
}
function showSubIssues () {
@ -71,7 +71,7 @@
value: subIssues.map((iss) => {
const text = currentTeam ? `${getIssueId(currentTeam, iss)} ${iss.title}` : iss.title
return { id: iss._id, text, isSelected: iss._id === issue._id, ...getIssueStatusIcon(iss) }
return { id: iss._id, text, isSelected: iss._id === value._id, ...getIssueStatusIcon(iss) }
}),
width: 'large'
},

View File

@ -82,7 +82,7 @@
function hourFloor (value: number): number {
const days = Math.ceil(value)
const hours = value - days
return days + Math.floor(hours * 10) / 10
return days + Math.floor(hours * 100) / 100
}
</script>

View File

@ -44,17 +44,19 @@
style:stroke-dasharray={lenghtC}
style:stroke-dashoffset={dashOffset === 0 ? 0 : dashOffset + 3}
/>
<circle
cx={8}
cy={8}
r={7}
class="progress-circle"
style:stroke={primary ? 'var(--primary-bg-color)' : color}
style:opacity={dashOffset === 0 ? 0 : 1}
style:transform={'rotate(-82deg)'}
style:stroke-dasharray={lenghtC}
style:stroke-dashoffset={dashOffset === 0 ? lenghtC : lenghtC - dashOffset + 1}
/>
{#if min !== max && min !== value}
<circle
cx={8}
cy={8}
r={7}
class="progress-circle"
style:stroke={primary ? 'var(--primary-bg-color)' : color}
style:opacity={dashOffset === 0 ? 0 : 1}
style:transform={'rotate(-82deg)'}
style:stroke-dasharray={lenghtC}
style:stroke-dashoffset={dashOffset === 0 ? lenghtC : lenghtC - dashOffset + 1}
/>
{/if}
</svg>
<style lang="scss">

View File

@ -78,7 +78,7 @@
okLabel={value === undefined ? presentation.string.Create : presentation.string.Save}
>
<div class="flex-row-center gap-2">
<EditBox bind:value={data.value} {placeholder} format={'number'} kind={'editbox'} {maxWidth} focus />
<EditBox focus bind:value={data.value} {placeholder} format={'number'} kind={'editbox'} {maxWidth} />
<UserBox
_class={contact.class.Employee}
label={contact.string.Employee}

View File

@ -4,15 +4,16 @@
import { Project } from '@anticrm/tracker'
import { Button, EditBox, Icon, showPopup } from '@anticrm/ui'
import { DocAttributeBar } from '@anticrm/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte'
import { activeProject } from '../../issues'
import tracker from '../../plugin'
import IssuesView from '../issues/IssuesView.svelte'
import ProjectPopup from './ProjectPopup.svelte'
import { activeProject } from '../../issues'
import { onDestroy } from 'svelte'
export let project: Project
const client = getClient()
const dispatch = createEventDispatcher()
async function change (field: string, value: any) {
await client.update(project, { [field]: value })
@ -21,6 +22,7 @@
showPopup(ProjectPopup, { _class: tracker.class.Project }, evt.target as HTMLElement, (value) => {
if (value != null) {
project = value
dispatch('project', project._id)
}
})
}

View File

@ -33,7 +33,7 @@
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = '100%'
export let onlyIcon: boolean = false
export let groupBy: string | undefined
export let groupBy: string | undefined = undefined
const client = getClient()

View File

@ -17,7 +17,7 @@
import { IntlString } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation'
import { Project } from '@anticrm/tracker'
import { closePopup, closeTooltip, location } from '@anticrm/ui'
import { closePopup, closeTooltip, getCurrentLocation, location, navigate } from '@anticrm/ui'
import { onDestroy } from 'svelte'
import tracker from '../../plugin'
import { ProjectsViewMode } from '../../utils'
@ -43,10 +43,8 @@
const projectQuery = createQuery()
$: if (projectId !== undefined) {
console.log('call query for', projectId)
projectQuery.query(tracker.class.Project, { _id: projectId }, (result) => {
project = result.shift()
console.log('recieve result for', projectId, project)
})
} else {
projectQuery.unsubscribe()
@ -55,7 +53,14 @@
</script>
{#if project}
<EditProject {project} />
<EditProject
{project}
on:project={(evt) => {
const loc = getCurrentLocation()
loc.path[5] = evt.detail
navigate(loc)
}}
/>
{:else}
<ProjectBrowser {label} {query} {search} {mode} />
{/if}

View File

@ -18,6 +18,7 @@
import { createQuery, getClient } from '@anticrm/presentation'
import { Issue, Sprint } from '@anticrm/tracker'
import { ButtonKind, ButtonShape, ButtonSize, isWeekend, Label, tooltip } from '@anticrm/ui'
import DatePresenter from '@anticrm/ui/src/components/calendar/DatePresenter.svelte'
import { activeSprint } from '../../issues'
import tracker from '../../plugin'
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
@ -58,13 +59,29 @@
$: 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)
$: totalEstimation = (noParents ?? [{ estimation: 0, childInfo: [] } as unknown as Issue])
.map((it) => {
if (it.childInfo?.length > 0) {
const cEstimation = it.childInfo.map((ct) => ct.estimation).reduce((a, b) => a + b, 0)
if (cEstimation !== 0) {
return cEstimation
}
}
return it.estimation
})
.reduce((it, cur) => {
return it + cur
})
$: totalReported = (noParents ?? [{ reportedTime: 0 }])
.map((it) => it.reportedTime)
$: totalReported = (noParents ?? [{ reportedTime: 0, childInfo: [] } as unknown as Issue])
.map((it) => {
if (it.childInfo?.length > 0) {
const cReported = it.childInfo.map((ct) => ct.reportedTime).reduce((a, b) => a + b, 0)
if (cReported !== 0) {
return cReported
}
}
return it.reportedTime
})
.reduce((it, cur) => {
return it + cur
})
@ -77,11 +94,12 @@
})
}
function getDayOfSprint (startDate: number, now: number): number {
const days = Math.floor(Math.abs((1 + now - startDate) / 1000 / 60 / 60 / 24)) + 1
const days = Math.floor(Math.abs((1 + now - startDate) / 1000 / 60 / 60 / 24))
const stDate = new Date(startDate)
const stDateDate = stDate.getDate()
const stTime = stDate.getTime()
const ds = Array.from(Array(days).keys()).map((it) => stDateDate + it)
return ds.filter((it) => !isWeekend(new Date(stDate.setDate(it)))).length
return ds.filter((it) => !isWeekend(new Date(new Date(stTime).setDate(it)))).length
}
</script>
@ -106,22 +124,29 @@
</div>
{/if}
{#if sprint}
{@const now = Date.now()}
<div class="flex-row-center">
<DatePresenter value={sprint.startDate} kind={'transparent'} />
<span class="p-1"> / </span><DatePresenter value={sprint.targetDate} kind={'transparent'} />
</div>
<div class="flex-row-center ml-2">
<!-- Active sprint in time -->
<Label
label={tracker.string.SprintPassed}
params={{
from:
now < sprint.startDate
? 0
: now > sprint.targetDate
? getDayOfSprint(sprint.startDate, sprint.targetDate)
: getDayOfSprint(sprint.startDate, now),
to: getDayOfSprint(sprint.startDate, sprint.targetDate)
}}
/>
</div>
{/if}
{#if issues}
{#if sprint}
{@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={{
from: getDayOfSprint(sprint.startDate, now),
to: getDayOfSprint(sprint.startDate, sprint.targetDate) - 1
}}
/>
</div>
{/if}
{/if}
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
<div class="ml-4 flex-row-center">
<div class="mr-2">

View File

@ -72,6 +72,8 @@ import SprintEditor from './components/sprints/SprintEditor.svelte'
import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.svelte'
import TimeSpendReport from './components/issues/timereport/TimeSpendReport.svelte'
import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte'
import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte'
import GrowPresenter from './components/issues/GrowPresenter.svelte'
export async function queryIssue<D extends Issue> (
_class: Ref<Class<D>>,
@ -177,7 +179,9 @@ export default async (): Promise<Resources> => ({
SprintEditor,
ReportedTimeEditor,
TimeSpendReport,
EstimationEditor
EstimationEditor,
SubIssuesSelector,
GrowPresenter
},
completion: {
IssueQuery: async (client: Client, query: string) => await queryIssue(tracker.class.Issue, client, query)

View File

@ -278,7 +278,8 @@ export default mergeIds(trackerId, tracker, {
SprintTitlePresenter: '' as AnyComponent,
ReportedTimeEditor: '' as AnyComponent,
TimeSpendReport: '' as AnyComponent,
EstimationEditor: '' as AnyComponent
EstimationEditor: '' as AnyComponent,
GrowPresenter: '' as AnyComponent
},
function: {
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>

View File

@ -32,7 +32,7 @@
<div
class="flex-no-shrink"
style="{justify !== '' ? `text-align: ${justify}; ` : ''}min-width: var(--fixed-{key});"
style="{justify !== '' ? `text-align: ${justify}; ` : ''} min-width: var(--fixed-{key});"
use:resizeObserver={(element) => {
cWidth = element.clientWidth
}}

View File

@ -212,7 +212,9 @@
on:click={() => changeSorting(attribute.sortingKey)}
>
<div class="antiTable-cells">
<Label label={attribute.label} />
{#if attribute.label}
<Label label={attribute.label} />
{/if}
{#if attribute.sortingKey === sortKey}
<div class="icon">
{#if sortOrder === SortingOrder.Ascending}

View File

@ -16,11 +16,13 @@
import core, {
DocumentUpdate,
Ref,
Space,
Tx,
TxCollectionCUD,
TxCreateDoc,
TxCUD,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc,
WithLookup
} from '@anticrm/core'
@ -63,19 +65,8 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
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
}
}
})
)
}
await updateIssueParentEstimations(issue, res, control, [], issue.parents)
return res
}
}
@ -85,6 +76,29 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
return await doIssueUpdate(updateTx, control)
}
}
if (actualTx._class === core.class.TxRemoveDoc) {
const removeTx = actualTx as TxRemoveDoc<Issue>
if (control.hierarchy.isDerived(removeTx.objectClass, tracker.class.Issue)) {
const parentIssue = await control.findAll(tracker.class.Issue, {
'childInfo.childId': removeTx.objectId
})
const res: Tx[] = []
const parents: IssueParentInfo[] = parentIssue.map((it) => ({ parentId: it._id, parentTitle: it.title }))
await updateIssueParentEstimations(
{
_id: removeTx.objectId,
estimation: 0,
reportedTime: 0,
space: removeTx.space
},
res,
control,
parents,
[]
)
return res
}
}
return []
}
@ -252,7 +266,12 @@ async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerCont
return res
}
function updateIssueParentEstimations (
issue: WithLookup<Issue>,
issue: {
_id: Ref<Issue>
space: Ref<Space>
estimation: number
reportedTime: number
},
res: Tx[],
control: TriggerControl,
sourceParents: IssueParentInfo[],

View File

@ -70,9 +70,11 @@ function isLookupSort<T extends Doc> (sort: SortingQuery<T> | undefined): boolea
interface LookupStep {
from: string
localField: string
foreignField: string
localField?: string
foreignField?: string
as: string
let?: any
pipeline?: any
}
abstract class MongoAdapterBase extends TxProcessor {
@ -193,12 +195,23 @@ abstract class MongoAdapterBase extends TxProcessor {
_class = value
}
const domain = this.hierarchy.getDomain(_class)
const desc = this.hierarchy.getDescendants(_class)
if (domain !== DOMAIN_MODEL) {
const step = {
const asVal = as.split('.').join('') + '_lookup'
const step: LookupStep = {
from: domain,
localField: fullKey,
foreignField: attr,
as: as.split('.').join('') + '_lookup'
// localField: fullKey,
// foreignField: attr,
let: { docId: '$' + fullKey },
pipeline: [
{
$match: {
_class: { $in: desc },
$expr: { $eq: ['$$docId', '$' + attr] }
}
}
],
as: asVal
}
result.push(step)
}