mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Initial Estimations support. (#2251)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
42ba7c0944
commit
681d83a5d3
@ -206,6 +206,7 @@ export function createModel (builder: Builder): void {
|
||||
'',
|
||||
{
|
||||
key: '$lookup.contact.$lookup.channels',
|
||||
label: contact.string.Channel,
|
||||
sortingKey: ['$lookup.contact.$lookup.channels.lastMessage', '$lookup.contact.channels']
|
||||
},
|
||||
'modifiedOn'
|
||||
@ -232,7 +233,11 @@ export function createModel (builder: Builder): void {
|
||||
'attachments',
|
||||
'modifiedOn',
|
||||
{ key: '', presenter: view.component.RolePresenter, label: view.string.Role },
|
||||
{ key: '$lookup.channels', sortingKey: ['$lookup.channels.lastMessage', 'channels'] }
|
||||
{
|
||||
key: '$lookup.channels',
|
||||
label: contact.string.ContactInfo,
|
||||
sortingKey: ['$lookup.channels.lastMessage', 'channels']
|
||||
}
|
||||
],
|
||||
hiddenKeys: ['name']
|
||||
},
|
||||
|
@ -342,6 +342,7 @@ export function createModel (builder: Builder): void {
|
||||
'',
|
||||
{
|
||||
key: '$lookup.channels',
|
||||
label: contact.string.ContactInfo,
|
||||
sortingKey: ['$lookup.channels.lastMessage', 'channels']
|
||||
},
|
||||
'modifiedOn'
|
||||
|
@ -182,7 +182,11 @@ export function createModel (builder: Builder): void {
|
||||
'$lookup._class',
|
||||
'leads',
|
||||
'modifiedOn',
|
||||
{ key: '$lookup.channels', sortingKey: ['$lookup.channels.lastMessage', 'channels'] }
|
||||
{
|
||||
key: '$lookup.channels',
|
||||
label: contact.string.ContactInfo,
|
||||
sortingKey: ['$lookup.channels.lastMessage', 'channels']
|
||||
}
|
||||
],
|
||||
hiddenKeys: ['name']
|
||||
},
|
||||
|
@ -283,7 +283,11 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
},
|
||||
'modifiedOn',
|
||||
{ key: '$lookup.channels', sortingKey: ['$lookup.channels.lastMessage', 'channels'] }
|
||||
{
|
||||
key: '$lookup.channels',
|
||||
label: contact.string.ContactInfo,
|
||||
sortingKey: ['$lookup.channels.lastMessage', 'channels']
|
||||
}
|
||||
],
|
||||
hiddenKeys: ['name']
|
||||
},
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
Index,
|
||||
Model,
|
||||
Prop,
|
||||
ReadOnly,
|
||||
TypeDate,
|
||||
TypeMarkup,
|
||||
TypeNumber,
|
||||
@ -34,11 +35,12 @@ import {
|
||||
import attachment from '@anticrm/model-attachment'
|
||||
import chunter from '@anticrm/model-chunter'
|
||||
import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace, TType } from '@anticrm/model-core'
|
||||
import view, { createAction } from '@anticrm/model-view'
|
||||
import view, { classPresenter, createAction } from '@anticrm/model-view'
|
||||
import workbench, { createNavigateAction } from '@anticrm/model-workbench'
|
||||
import notification from '@anticrm/notification'
|
||||
import { Asset, IntlString } from '@anticrm/platform'
|
||||
import setting from '@anticrm/setting'
|
||||
import tags from '@anticrm/tags'
|
||||
import task from '@anticrm/task'
|
||||
import {
|
||||
Document,
|
||||
@ -52,10 +54,10 @@ import {
|
||||
Sprint,
|
||||
SprintStatus,
|
||||
Team,
|
||||
TimeSpendReport,
|
||||
trackerId
|
||||
} from '@anticrm/tracker'
|
||||
import { KeyBinding } from '@anticrm/view'
|
||||
import tags from '@anticrm/tags'
|
||||
import tracker from './plugin'
|
||||
|
||||
import presentation from '@anticrm/model-presentation'
|
||||
@ -159,6 +161,13 @@ export class TTeam extends TSpace implements Team {
|
||||
defaultIssueStatus!: Ref<IssueStatus>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function TypeReportedTime (): Type<number> {
|
||||
return { _class: tracker.class.TypeReportedTime, label: core.string.Number }
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -219,8 +228,38 @@ export class TIssue extends TAttachedDoc implements Issue {
|
||||
|
||||
@Prop(TypeRef(tracker.class.Sprint), tracker.string.Sprint)
|
||||
sprint!: Ref<Sprint> | null
|
||||
|
||||
@Prop(TypeNumber(), tracker.string.Estimation)
|
||||
estimation!: number
|
||||
|
||||
@Prop(TypeReportedTime(), tracker.string.ReportedTime)
|
||||
@ReadOnly()
|
||||
reportedTime!: number
|
||||
|
||||
@Prop(Collection(tracker.class.TimeSpendReport), tracker.string.TimeSpendReports)
|
||||
reports!: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@Model(tracker.class.TimeSpendReport, core.class.AttachedDoc, DOMAIN_TRACKER)
|
||||
@UX(tracker.string.TimeSpendReport, tracker.icon.TimeReport, tracker.string.TimeSpendReport)
|
||||
export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport {
|
||||
declare attachedTo: Ref<Issue>
|
||||
|
||||
@Prop(TypeRef(contact.class.Employee), contact.string.Employee)
|
||||
employee!: Ref<Employee>
|
||||
|
||||
@Prop(TypeDate(), tracker.string.TimeSpendReportDate)
|
||||
date!: Timestamp | null
|
||||
|
||||
@Prop(TypeNumber(), tracker.string.TimeSpendReportValue)
|
||||
value!: number
|
||||
|
||||
@Prop(TypeString(), tracker.string.TimeSpendReportDescription)
|
||||
description!: string
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -321,6 +360,10 @@ export class TSprint extends TDoc implements Sprint {
|
||||
declare space: Ref<Team>
|
||||
}
|
||||
|
||||
@UX(core.string.Number)
|
||||
@Model(tracker.class.TypeReportedTime, core.class.Type)
|
||||
export class TTypeReportedTime extends TType {}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TTeam,
|
||||
@ -331,7 +374,9 @@ export function createModel (builder: Builder): void {
|
||||
TTypeIssuePriority,
|
||||
TTypeProjectStatus,
|
||||
TSprint,
|
||||
TTypeSprintStatus
|
||||
TTypeSprintStatus,
|
||||
TTimeSpendReport,
|
||||
TTypeReportedTime
|
||||
)
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
@ -357,6 +402,7 @@ export function createModel (builder: Builder): void {
|
||||
presenter: tracker.component.SprintEditor,
|
||||
props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false }
|
||||
},
|
||||
{ key: '', presenter: tracker.component.EstimationEditor, props: { kind: 'list', size: 'small' } },
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter, props: { fixed: 'right' } },
|
||||
{
|
||||
key: '$lookup.assignee',
|
||||
@ -474,6 +520,10 @@ export function createModel (builder: Builder): void {
|
||||
presenter: tracker.component.IssuePreview
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.TimeSpendReport, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: tracker.component.TimeSpendReport
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectTitle, {
|
||||
titleProvider: tracker.function.IssueTitleProvider
|
||||
})
|
||||
@ -1015,4 +1065,11 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
tracker.action.Relations
|
||||
)
|
||||
|
||||
classPresenter(
|
||||
builder,
|
||||
tracker.class.TypeReportedTime,
|
||||
view.component.NumberPresenter,
|
||||
tracker.component.ReportedTimeEditor
|
||||
)
|
||||
}
|
||||
|
@ -317,6 +317,15 @@ async function upgradeProjects (tx: TxOperations): Promise<void> {
|
||||
|
||||
export const trackerOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await client.update(
|
||||
DOMAIN_TRACKER,
|
||||
{ _class: tracker.class.Issue, reports: { $exists: false } },
|
||||
{
|
||||
reports: 0,
|
||||
estimation: 0,
|
||||
reportedTime: 0
|
||||
}
|
||||
)
|
||||
await Promise.all([migrateIssueProjects(client), migrateParentIssues(client)])
|
||||
await migrateIssueParentInfo(client)
|
||||
},
|
||||
|
@ -65,6 +65,7 @@ export interface UXObject extends Obj {
|
||||
label: IntlString
|
||||
icon?: Asset
|
||||
hidden?: boolean
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,7 +80,7 @@ export default plugin(coreId, {
|
||||
Type: '' as Ref<Class<Type<any>>>,
|
||||
TypeString: '' as Ref<Class<Type<string>>>,
|
||||
TypeIntlString: '' as Ref<Class<Type<IntlString>>>,
|
||||
TypeNumber: '' as Ref<Class<Type<string>>>,
|
||||
TypeNumber: '' as Ref<Class<Type<number>>>,
|
||||
TypeMarkup: '' as Ref<Class<Type<string>>>,
|
||||
TypeBoolean: '' as Ref<Class<Type<boolean>>>,
|
||||
TypeTimestamp: '' as Ref<Class<Type<Timestamp>>>,
|
||||
|
@ -292,6 +292,48 @@ export abstract class TxProcessor implements WithTx {
|
||||
return rawDoc
|
||||
}
|
||||
|
||||
static buildDoc2Doc<D extends Doc>(txes: Tx[]): D | undefined {
|
||||
let doc: Doc
|
||||
let createTx = txes.find((tx) => tx._class === core.class.TxCreateDoc)
|
||||
const collectionTxes = false
|
||||
if (createTx === undefined) {
|
||||
const collectionTxes = txes.filter((tx) => tx._class === core.class.TxCollectionCUD) as Array<
|
||||
TxCollectionCUD<Doc, AttachedDoc>
|
||||
>
|
||||
createTx = collectionTxes.find((p) => p.tx._class === core.class.TxCreateDoc)
|
||||
}
|
||||
if (createTx === undefined) return
|
||||
doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc<Doc>)
|
||||
for (let tx of txes) {
|
||||
if (collectionTxes) {
|
||||
tx = TxProcessor.extractTx(tx)
|
||||
}
|
||||
if (tx._class === core.class.TxUpdateDoc) {
|
||||
doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc<Doc>)
|
||||
} else if (tx._class === core.class.TxMixin) {
|
||||
const mixinTx = tx as TxMixin<Doc, Doc>
|
||||
doc = TxProcessor.updateMixin4Doc(doc, mixinTx)
|
||||
}
|
||||
}
|
||||
return doc as D
|
||||
}
|
||||
|
||||
static extractTx (tx: Tx): Tx {
|
||||
if (tx._class === core.class.TxCollectionCUD) {
|
||||
const ctx = tx as TxCollectionCUD<Doc, AttachedDoc>
|
||||
if (ctx.tx._class === core.class.TxCreateDoc) {
|
||||
const create = ctx.tx as TxCreateDoc<AttachedDoc>
|
||||
create.attributes.attachedTo = ctx.objectId
|
||||
create.attributes.attachedToClass = ctx.objectClass
|
||||
create.attributes.collection = ctx.collection
|
||||
return create
|
||||
}
|
||||
return ctx.tx
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
protected abstract txCreateDoc (tx: TxCreateDoc<Doc>): Promise<TxResult>
|
||||
protected abstract txPutBag (tx: TxPutBag<PropertyType>): Promise<TxResult>
|
||||
protected abstract txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult>
|
||||
|
@ -158,6 +158,15 @@ export function Hidden () {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function ReadOnly () {
|
||||
return function (target: any, propertyKey: string): void {
|
||||
setAttr(target, propertyKey, 'readonly', true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -92,6 +92,7 @@
|
||||
type={attribute?.type}
|
||||
{maxWidth}
|
||||
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
|
||||
readonly={attribute.readonly ?? false}
|
||||
space={object.space}
|
||||
{onChange}
|
||||
{focus}
|
||||
@ -105,6 +106,7 @@
|
||||
type={attribute?.type}
|
||||
{maxWidth}
|
||||
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
|
||||
readonly={attribute.readonly ?? false}
|
||||
space={object.space}
|
||||
{onChange}
|
||||
{focus}
|
||||
|
@ -27,7 +27,7 @@
|
||||
if (value < min) value = min
|
||||
|
||||
const lenghtC: number = Math.PI * 14 - 1
|
||||
const procC: number = lenghtC / (max - min)
|
||||
$: procC = lenghtC / (max - min)
|
||||
$: dashOffset = (value - min) * procC
|
||||
</script>
|
||||
|
||||
|
@ -162,4 +162,9 @@
|
||||
<path d="M1.33398 10H0.833984V10.5H1.33398V10ZM1.33398 10.5H7.33398V9.5H1.33398V10.5ZM7.33398 5.5H3.33398V6.5H7.33398V5.5ZM0.833984 8V10H1.83398V8H0.833984ZM3.33398 5.5C1.95327 5.5 0.833984 6.61929 0.833984 8H1.83398C1.83398 7.17157 2.50556 6.5 3.33398 6.5V5.5Z" fill="white"/>
|
||||
<path d="M14.666 6H15.166V5.5H14.666V6ZM14.666 5.5H11.3327V6.5H14.666V5.5ZM11.3327 10.5H12.666V9.5H11.3327V10.5ZM15.166 8V6H14.166V8H15.166ZM12.666 10.5C14.0467 10.5 15.166 9.38071 15.166 8H14.166C14.166 8.82843 13.4944 9.5 12.666 9.5V10.5Z" fill="white"/>
|
||||
</symbol>
|
||||
<symbol id="timeReport" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="7.99935" cy="7.99996" r="5.83333" stroke="white"/>
|
||||
<path d="M10.8284 5.17157C10.0783 4.42143 9.06087 4 8 4C6.93913 4 5.92172 4.42143 5.17157 5.17157C4.42143 5.92172 4 6.93913 4 8C4 9.06087 4.42143 10.0783 5.17157 10.8284L8 8L10.8284 5.17157Z" fill="white"/>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@ -196,7 +196,19 @@
|
||||
"AddToSprint": "Add to Sprint",
|
||||
|
||||
"NewSprint": "New Sprint",
|
||||
"CreateSprint": "Create"
|
||||
"CreateSprint": "Create",
|
||||
|
||||
"Estimation": "Estimation",
|
||||
"ReportedTime": "Reported Time",
|
||||
"TimeSpendReports": "Time spend reports",
|
||||
"TimeSpendReport": "Time spend report",
|
||||
"TimeSpendReportAdd": "Add time report",
|
||||
"TimeSpendReportDate": "Date",
|
||||
"TimeSpendReportValue": "Reported time",
|
||||
"TimeSpendReportValueTooltip": "Reported time in man days",
|
||||
"TimeSpendReportDescription": "Description",
|
||||
"TimeSpendValue": "{value}d",
|
||||
"SprintPassed": "{from}d/{to}d"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -196,7 +196,19 @@
|
||||
"AddToSprint": "Добавить в Спринт",
|
||||
|
||||
"NewSprint": "Новый Спринт",
|
||||
"CreateSprint": "Создать"
|
||||
"CreateSprint": "Создать",
|
||||
|
||||
"Estimation": "Оценка",
|
||||
"ReportedTime": "Использовано",
|
||||
"TimeSpendReports": "Отчеты по времени",
|
||||
"TimeSpendReport": "Отчет по времени",
|
||||
"TimeSpendReportAdd": "Добавить завтраченное время",
|
||||
"TimeSpendReportDate": "Дата",
|
||||
"TimeSpendReportValue": "Затраченное время",
|
||||
"TimeSpendReportValueTooltip": "Затраченное время в человеко днях",
|
||||
"TimeSpendReportDescription": "Описание",
|
||||
"TimeSpendValue": "{value}d",
|
||||
"SprintPassed": "{from}d/{to}d"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -66,7 +66,9 @@ loadMetadata(tracker.icon, {
|
||||
|
||||
CopyID: `${icons}#copyID`,
|
||||
CopyURL: `${icons}#copyURL`,
|
||||
CopyBranch: `${icons}#copyBranch`
|
||||
CopyBranch: `${icons}#copyBranch`,
|
||||
TimeReport: `${icons}#timeReport`,
|
||||
Estimation: `${icons}#timeReport`
|
||||
})
|
||||
|
||||
addStringsLoader(trackerId, async (lang: string) => await import(`../lang/${lang}.json`))
|
||||
|
@ -68,7 +68,10 @@
|
||||
dueDate: null,
|
||||
comments: 0,
|
||||
subIssues: 0,
|
||||
parents: []
|
||||
parents: [],
|
||||
reportedTime: 0,
|
||||
estimation: 0,
|
||||
reports: 0
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -151,7 +154,10 @@
|
||||
dueDate: object.dueDate,
|
||||
parents: parentIssue
|
||||
? [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
|
||||
: []
|
||||
: [],
|
||||
reportedTime: 0,
|
||||
estimation: 0,
|
||||
reports: 0
|
||||
}
|
||||
|
||||
await client.addCollection(
|
||||
|
@ -184,6 +184,7 @@
|
||||
shouldShowLabel: true,
|
||||
value: groupByKey ? { [groupByKey]: category } : {},
|
||||
statuses: groupByKey === 'status' ? statuses : undefined,
|
||||
issues: groupedIssues[category],
|
||||
size: 'inline',
|
||||
kind: 'list'
|
||||
}}
|
||||
@ -241,6 +242,7 @@
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
groupBy={groupByKey}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
@ -255,6 +257,7 @@
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
groupBy={groupByKey}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</FixedColumn>
|
||||
@ -263,6 +266,7 @@
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
groupBy={groupByKey}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
{:else if attributeModel.props?.fixed}
|
||||
@ -275,6 +279,7 @@
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
groupBy={groupByKey}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</FixedColumn>
|
||||
@ -284,6 +289,7 @@
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
issueId={docObject._id}
|
||||
groupBy={groupByKey}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
|
@ -57,7 +57,8 @@
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus,
|
||||
space: tracker.class.Team
|
||||
space: tracker.class.Team,
|
||||
sprint: tracker.class.Sprint
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -0,0 +1,126 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Issue } from '@anticrm/tracker'
|
||||
import { Button, ButtonKind, ButtonSize, eventToHTMLElement, Label, showPopup } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationPopup from './EstimationPopup.svelte'
|
||||
import EstimationProgressCircle from './EstimationProgressCircle.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let isEditable: boolean = true
|
||||
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
export let width: string | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const handleestimationEditorOpened = (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(
|
||||
EstimationPopup,
|
||||
{ value: value.estimation, format: 'number', object: value },
|
||||
eventToHTMLElement(event),
|
||||
(res) => {
|
||||
if (res != null) {
|
||||
changeEstimation(res)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const changeEstimation = async (newEstimation: number | undefined) => {
|
||||
if (!isEditable || newEstimation === undefined || value.estimation === newEstimation) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('change', newEstimation)
|
||||
|
||||
if ('_id' in value) {
|
||||
await client.update(value, { estimation: newEstimation })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if kind === 'list'}
|
||||
<div class="estimation-container" on:click={handleestimationEditorOpened}>
|
||||
<div class="icon">
|
||||
<EstimationProgressCircle value={value.reportedTime} max={value.estimation} />
|
||||
</div>
|
||||
<span class="overflow-label label">
|
||||
{#if value.reportedTime > 0}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: value.reportedTime }} />
|
||||
/
|
||||
{/if}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: value.estimation }} />
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<Button
|
||||
showTooltip={isEditable ? { label: tracker.string.Estimation } : undefined}
|
||||
label={tracker.string.TimeSpendValue}
|
||||
labelParams={{ value: value.estimation }}
|
||||
icon={tracker.icon.Estimation}
|
||||
{justify}
|
||||
{width}
|
||||
{size}
|
||||
{kind}
|
||||
disabled={!isEditable}
|
||||
on:click={handleestimationEditorOpened}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.estimation-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--content-color);
|
||||
}
|
||||
.label {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
&:hover {
|
||||
.icon {
|
||||
color: var(--caption-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,92 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@anticrm/contact'
|
||||
import { FindOptions } from '@anticrm/core'
|
||||
import { Card } from '@anticrm/presentation'
|
||||
import { Issue, TimeSpendReport } from '@anticrm/tracker'
|
||||
import { Button, EditBox, EditStyle, eventToHTMLElement, IconAdd, Scroller, showPopup } from '@anticrm/ui'
|
||||
import { TableBrowser } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../../plugin'
|
||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||
import presentation from '@anticrm/presentation'
|
||||
|
||||
export let value: string | number | undefined
|
||||
export let format: 'text' | 'password' | 'number'
|
||||
export let kind: EditStyle = 'search-style'
|
||||
export let maxWidth: string = '10rem'
|
||||
export let object: Issue
|
||||
|
||||
let _value = value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function _onkeypress (ev: KeyboardEvent) {
|
||||
if (ev.key === 'Enter') dispatch('close', _value)
|
||||
}
|
||||
const options: FindOptions<TimeSpendReport> = {
|
||||
lookup: { employee: contact.class.Employee }
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={tracker.string.Estimation}
|
||||
canSave={true}
|
||||
okAction={() => {
|
||||
dispatch('close', _value)
|
||||
}}
|
||||
okLabel={presentation.string.Save}
|
||||
on:close={() => {
|
||||
dispatch('close', null)
|
||||
}}
|
||||
>
|
||||
<div class="header no-border flex-col p-1">
|
||||
<div class="flex-row-center flex-between">
|
||||
<EditBox
|
||||
bind:value={_value}
|
||||
{format}
|
||||
{kind}
|
||||
{maxWidth}
|
||||
placeholder={tracker.string.Estimation}
|
||||
focus
|
||||
on:keypress={_onkeypress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Scroller tableFade>
|
||||
<TableBrowser
|
||||
_class={tracker.class.TimeSpendReport}
|
||||
query={{ attachedTo: object._id }}
|
||||
config={['', '$lookup.employee', 'date', 'description']}
|
||||
{options}
|
||||
/>
|
||||
</Scroller>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
on:click={(event) => {
|
||||
showPopup(
|
||||
TimeSpendReportPopup,
|
||||
{ issueId: object._id, issueClass: object._class, space: object.space },
|
||||
eventToHTMLElement(event)
|
||||
)
|
||||
}}
|
||||
label={tracker.string.TimeSpendReportAdd}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Card>
|
@ -0,0 +1,71 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { FernColor, FlamingoColor, IconSize, PlatinumColor } from '@anticrm/ui'
|
||||
|
||||
export let value: number
|
||||
export let min: number = 0
|
||||
export let max: number = 100
|
||||
export let size: IconSize = 'small'
|
||||
export let primary: boolean = false
|
||||
|
||||
export let color: string = PlatinumColor
|
||||
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
|
||||
|
||||
$: color = value > max ? overdueColor : value === min ? color : greenColor
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" fill="none" viewBox="0 0 16 16">
|
||||
<circle
|
||||
cx={8}
|
||||
cy={8}
|
||||
r={7}
|
||||
class="progress-circle"
|
||||
style:stroke={'var(--divider-color)'}
|
||||
style:opacity={'.5'}
|
||||
style:transform={`rotate(${-78 + ((dashOffset + 1) * 360) / (lenghtC + 1)}deg)`}
|
||||
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}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<style lang="scss">
|
||||
.progress-circle {
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: round;
|
||||
transform-origin: center;
|
||||
transition: transform 0.6s ease 0s, stroke-dashoffset 0.6s ease 0s, stroke-dasharray 0.6s ease 0s,
|
||||
opacity 0.6s ease 0s;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,82 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { Issue } from '@anticrm/tracker'
|
||||
import { ActionIcon, eventToHTMLElement, IconAdd, Label, showPopup } from '@anticrm/ui'
|
||||
import ReportsPopup from './ReportsPopup.svelte'
|
||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||
|
||||
// export let label: IntlString
|
||||
export let placeholder: IntlString
|
||||
export let object: Issue
|
||||
export let value: number
|
||||
export let kind: 'no-border' | 'link' = 'no-border'
|
||||
|
||||
function addTimeReport (event: MouseEvent): void {
|
||||
showPopup(
|
||||
TimeSpendReportPopup,
|
||||
{ issueId: object._id, issueClass: object._class, space: object.space },
|
||||
eventToHTMLElement(event)
|
||||
)
|
||||
}
|
||||
function showReports (event: MouseEvent): void {
|
||||
showPopup(ReportsPopup, { issue: object }, eventToHTMLElement(event))
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if kind === 'link'}
|
||||
<div class="link-container flex-between" on:click={showReports}>
|
||||
{#if value !== undefined}
|
||||
<span class="overflow-label">{value}</span>
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
<div class="add-action">
|
||||
<ActionIcon icon={IconAdd} size={'small'} action={addTimeReport} />
|
||||
</div>
|
||||
</div>
|
||||
{:else if value !== undefined}
|
||||
<span class="overflow-label">{value}</span>
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.link-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.875rem;
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
.add-action {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
background-color: var(--body-color);
|
||||
border-color: var(--divider-color);
|
||||
.add-action {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,59 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@anticrm/contact'
|
||||
import { FindOptions } from '@anticrm/core'
|
||||
import presentation, { Card } from '@anticrm/presentation'
|
||||
import { Issue, TimeSpendReport } from '@anticrm/tracker'
|
||||
import { Button, eventToHTMLElement, IconAdd, Scroller, showPopup } from '@anticrm/ui'
|
||||
import { TableBrowser } from '@anticrm/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||
export let issue: Issue
|
||||
|
||||
export function canClose (): boolean {
|
||||
return true
|
||||
}
|
||||
const options: FindOptions<TimeSpendReport> = {
|
||||
lookup: { employee: contact.class.Employee }
|
||||
}
|
||||
function addReport (event: MouseEvent): void {
|
||||
showPopup(
|
||||
TimeSpendReportPopup,
|
||||
{ issueId: issue._id, issueClass: issue._class, space: issue.space },
|
||||
eventToHTMLElement(event)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={tracker.string.TimeSpendReports}
|
||||
canSave={true}
|
||||
on:close
|
||||
okAction={() => {}}
|
||||
okLabel={presentation.string.Ok}
|
||||
>
|
||||
<Scroller tableFade>
|
||||
<TableBrowser
|
||||
_class={tracker.class.TimeSpendReport}
|
||||
query={{ attachedTo: issue._id }}
|
||||
config={['', '$lookup.employee', 'description', 'date', 'modifiedOn', 'modifiedBy']}
|
||||
{options}
|
||||
/>
|
||||
</Scroller>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button icon={IconAdd} size={'large'} on:click={addReport} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
@ -0,0 +1,86 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import { WithLookup } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import type { TimeSpendReport } from '@anticrm/tracker'
|
||||
import { eventToHTMLElement, Label, showPopup, tooltip } from '@anticrm/ui'
|
||||
import view, { AttributeModel } from '@anticrm/view'
|
||||
import { getObjectPresenter } from '@anticrm/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
|
||||
|
||||
export let value: WithLookup<TimeSpendReport>
|
||||
const client = getClient()
|
||||
let presenter: AttributeModel
|
||||
|
||||
getObjectPresenter(client, contact.class.Employee, { key: '' }).then((p) => {
|
||||
presenter = p
|
||||
})
|
||||
|
||||
function editSpendReport (event: MouseEvent): void {
|
||||
showPopup(
|
||||
TimeSpendReportPopup,
|
||||
{ issue: value.attachedTo, issueClass: value.attachedToClass, value: value },
|
||||
eventToHTMLElement(event)
|
||||
)
|
||||
}
|
||||
|
||||
let employee: Employee | undefined | null = value.$lookup?.employee ?? null
|
||||
$: if (employee === undefined) {
|
||||
client.findOne(value.attachedToClass, { _id: value.attachedTo }).then((r) => {
|
||||
employee = r as Employee
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<span
|
||||
class="issuePresenterRoot flex-row-center"
|
||||
on:click={editSpendReport}
|
||||
use:tooltip={value.employee
|
||||
? {
|
||||
label: tracker.string.TimeSpendReport,
|
||||
component: view.component.ObjectPresenter,
|
||||
props: {
|
||||
objectId: value.employee,
|
||||
_class: contact.class.Employee,
|
||||
value: value.$lookup?.employee
|
||||
}
|
||||
}
|
||||
: undefined}
|
||||
>
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: value.value }} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.issuePresenterRoot {
|
||||
white-space: nowrap;
|
||||
|
||||
font-size: 0.8125rem;
|
||||
color: var(--content-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:active {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,96 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@anticrm/contact'
|
||||
import { AttachedData, Class, DocumentUpdate, Ref, Space } from '@anticrm/core'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import presentation, { Card, getClient, UserBox } from '@anticrm/presentation'
|
||||
import { Issue, TimeSpendReport } from '@anticrm/tracker'
|
||||
import { DatePresenter, EditBox } from '@anticrm/ui'
|
||||
import tracker from '../../../plugin'
|
||||
|
||||
export let issueId: Ref<Issue>
|
||||
export let issueClass: Ref<Class<Issue>>
|
||||
export let space: Ref<Space>
|
||||
|
||||
export let value: TimeSpendReport | undefined
|
||||
export let placeholder: IntlString = tracker.string.TimeSpendReportValue
|
||||
export let maxWidth: string = '10rem'
|
||||
|
||||
export function canClose (): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
const data = {
|
||||
date: value?.date ?? Date.now(),
|
||||
description: value?.description ?? '',
|
||||
value: value?.value,
|
||||
employee: value?.employee ?? null
|
||||
}
|
||||
async function create (): Promise<void> {
|
||||
if (value === undefined) {
|
||||
getClient().addCollection(
|
||||
tracker.class.TimeSpendReport,
|
||||
space,
|
||||
issueId,
|
||||
issueClass,
|
||||
'reports',
|
||||
data as AttachedData<TimeSpendReport>
|
||||
)
|
||||
} else {
|
||||
const ops: DocumentUpdate<TimeSpendReport> = {}
|
||||
if (value.value !== data.value) {
|
||||
ops.value = data.value
|
||||
}
|
||||
if (value.employee !== data.employee) {
|
||||
ops.employee = data.employee
|
||||
}
|
||||
if (value.description !== data.description) {
|
||||
ops.description = data.description
|
||||
}
|
||||
if (value.date !== data.date) {
|
||||
ops.date = data.date
|
||||
}
|
||||
if (Object.keys(ops).length > 0) {
|
||||
getClient().update(value, ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={tracker.string.TimeSpendReportAdd}
|
||||
canSave={data.value !== 0}
|
||||
okAction={create}
|
||||
on:close
|
||||
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 />
|
||||
<UserBox
|
||||
_class={contact.class.Employee}
|
||||
label={contact.string.Employee}
|
||||
kind={'link-bordered'}
|
||||
bind:value={data.employee}
|
||||
/>
|
||||
<DatePresenter kind={'link'} bind:value={data.date} editable />
|
||||
</div>
|
||||
<EditBox
|
||||
bind:value={data.description}
|
||||
placeholder={tracker.string.TimeSpendReportDescription}
|
||||
kind={'editbox'}
|
||||
{maxWidth}
|
||||
/>
|
||||
</Card>
|
@ -20,6 +20,7 @@
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import tracker from '../../plugin'
|
||||
import ProjectSelector from '../ProjectSelector.svelte'
|
||||
import { activeProject } from '../../issues'
|
||||
|
||||
export let value: Issue
|
||||
export let isEditable: boolean = true
|
||||
@ -32,6 +33,7 @@
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
export let width: string | undefined = '100%'
|
||||
export let onlyIcon: boolean = false
|
||||
export let groupBy: string | undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@ -52,7 +54,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value.project || shouldShowPlaceholder}
|
||||
{#if (value.project && value.project !== $activeProject && groupBy !== 'project') || shouldShowPlaceholder}
|
||||
<div
|
||||
class="clear-mins"
|
||||
use:tooltip={{ label: value.project ? tracker.string.MoveToProject : tracker.string.AddToProject }}
|
||||
|
@ -14,11 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { Issue, Sprint } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { ButtonKind, ButtonShape, ButtonSize, tooltip } from '@anticrm/ui'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { Issue, Sprint } from '@anticrm/tracker'
|
||||
import { ButtonKind, ButtonShape, ButtonSize, isWeekend, Label, tooltip } from '@anticrm/ui'
|
||||
import { activeSprint } from '../../issues'
|
||||
import tracker from '../../plugin'
|
||||
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
|
||||
import SprintSelector from './SprintSelector.svelte'
|
||||
|
||||
export let value: Issue
|
||||
@ -32,6 +34,8 @@
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
export let width: string | undefined = '100%'
|
||||
export let onlyIcon: boolean = false
|
||||
export let issues: Issue[] | undefined
|
||||
export let groupBy: string | undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@ -50,9 +54,35 @@
|
||||
{ sprint: newSprintId }
|
||||
)
|
||||
}
|
||||
|
||||
$: totalEstimation = (issues ?? [{ estimation: 0 }])
|
||||
.map((it) => it.estimation)
|
||||
.reduce((it, cur) => {
|
||||
return it + cur
|
||||
})
|
||||
$: totalReported = (issues ?? [{ reportedTime: 0 }])
|
||||
.map((it) => it.reportedTime)
|
||||
.reduce((it, cur) => {
|
||||
return it + cur
|
||||
})
|
||||
|
||||
const sprintQuery = createQuery()
|
||||
let sprint: Sprint | undefined
|
||||
$: if (issues !== undefined && value.sprint) {
|
||||
sprintQuery.query(tracker.class.Sprint, { _id: value.sprint }, (res) => {
|
||||
sprint = res.shift()
|
||||
})
|
||||
}
|
||||
function getDayOfSprint (startDate: number, now: number): number {
|
||||
const days = Math.floor(Math.abs((1 + now - startDate) / 1000 / 60 / 60 / 24)) + 1
|
||||
const stDate = new Date(startDate)
|
||||
const stDateDate = stDate.getDate()
|
||||
const ds = Array.from(Array(days).keys()).map((it) => stDateDate + it)
|
||||
return ds.filter((it) => !isWeekend(new Date(stDate.setDate(it)))).length
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value.sprint || shouldShowPlaceholder}
|
||||
{#if (value.sprint && value.sprint !== $activeSprint && groupBy !== 'sprint') || shouldShowPlaceholder}
|
||||
<div
|
||||
class="clear-mins"
|
||||
use:tooltip={{ label: value.sprint ? tracker.string.MoveToSprint : tracker.string.AddToSprint }}
|
||||
@ -72,3 +102,30 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if issues}
|
||||
{#if sprint}
|
||||
{@const now = Date.now()}
|
||||
{#if sprint.startDate < now && now < sprint.targetDate}
|
||||
<!-- Active sprint in time -->
|
||||
<Label
|
||||
label={tracker.string.SprintPassed}
|
||||
params={{
|
||||
from: getDayOfSprint(sprint.startDate, now),
|
||||
to: getDayOfSprint(sprint.startDate, sprint.targetDate) - 1
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
|
||||
<div class="ml-4 flex-row-center">
|
||||
<div class="mr-2">
|
||||
<EstimationProgressCircle value={totalReported} max={totalEstimation} />
|
||||
</div>
|
||||
{#if totalReported > 0}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: totalReported }} />
|
||||
/
|
||||
{/if}
|
||||
<Label label={tracker.string.TimeSpendValue} params={{ value: totalEstimation }} />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -69,6 +69,10 @@ import SprintTitlePresenter from './components/sprints/SprintTitlePresenter.svel
|
||||
import SprintSelector from './components/sprints/SprintSelector.svelte'
|
||||
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'
|
||||
|
||||
export async function queryIssue<D extends Issue> (
|
||||
_class: Ref<Class<D>>,
|
||||
client: Client,
|
||||
@ -170,7 +174,10 @@ export default async (): Promise<Resources> => ({
|
||||
SprintStatusPresenter,
|
||||
SprintTitlePresenter,
|
||||
SprintSelector,
|
||||
SprintEditor
|
||||
SprintEditor,
|
||||
ReportedTimeEditor,
|
||||
TimeSpendReport,
|
||||
EstimationEditor
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: async (client: Client, query: string) => await queryIssue(tracker.class.Issue, client, query)
|
||||
|
@ -213,7 +213,18 @@ export default mergeIds(trackerId, tracker, {
|
||||
CreateSprint: '' as IntlString,
|
||||
AddToSprint: '' as IntlString,
|
||||
MoveToSprint: '' as IntlString,
|
||||
NoSprint: '' as IntlString
|
||||
NoSprint: '' as IntlString,
|
||||
|
||||
Estimation: '' as IntlString,
|
||||
ReportedTime: '' as IntlString,
|
||||
TimeSpendReport: '' as IntlString,
|
||||
TimeSpendReportAdd: '' as IntlString,
|
||||
TimeSpendReports: '' as IntlString,
|
||||
TimeSpendReportDate: '' as IntlString,
|
||||
TimeSpendReportValue: '' as IntlString,
|
||||
TimeSpendReportDescription: '' as IntlString,
|
||||
TimeSpendValue: '' as IntlString,
|
||||
SprintPassed: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
NopeComponent: '' as AnyComponent,
|
||||
@ -261,7 +272,10 @@ export default mergeIds(trackerId, tracker, {
|
||||
Sprints: '' as AnyComponent,
|
||||
SprintPresenter: '' as AnyComponent,
|
||||
SprintStatusPresenter: '' as AnyComponent,
|
||||
SprintTitlePresenter: '' as AnyComponent
|
||||
SprintTitlePresenter: '' as AnyComponent,
|
||||
ReportedTimeEditor: '' as AnyComponent,
|
||||
TimeSpendReport: '' as AnyComponent,
|
||||
EstimationEditor: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>
|
||||
|
@ -299,7 +299,7 @@ const listIssueStatusOrder = [
|
||||
|
||||
export function getCategories (
|
||||
key: IssuesGroupByKeys | undefined,
|
||||
elements: Issue[],
|
||||
elements: Array<WithLookup<Issue>>,
|
||||
shouldShowAll: boolean,
|
||||
statuses: IssueStatus[],
|
||||
employees: Employee[]
|
||||
@ -307,7 +307,6 @@ export function getCategories (
|
||||
if (key === undefined) {
|
||||
return [undefined] // No grouping
|
||||
}
|
||||
|
||||
const defaultStatuses = listIssueStatusOrder.map(
|
||||
(category) => statuses.find((status) => status.category === category)?._id
|
||||
)
|
||||
@ -348,6 +347,17 @@ export function getCategories (
|
||||
})
|
||||
}
|
||||
|
||||
if (key === 'sprint') {
|
||||
const sprints = new Map(elements.map((x) => [x.sprint, x.$lookup?.sprint]))
|
||||
|
||||
existingCategories.sort((p1, p2) => {
|
||||
const i1 = sprints.get(p1 as Ref<Sprint>)
|
||||
const i2 = sprints.get(p2 as Ref<Sprint>)
|
||||
|
||||
return (i2?.startDate ?? 0) - (i1?.startDate ?? 0)
|
||||
})
|
||||
}
|
||||
|
||||
if (key === 'assignee') {
|
||||
existingCategories.sort((a1, a2) => {
|
||||
const employeeId1 = a1 as Ref<Employee> | null
|
||||
|
@ -157,6 +157,30 @@ export interface Issue extends AttachedDoc {
|
||||
rank: string
|
||||
|
||||
sprint?: Ref<Sprint> | null
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Declares time spend entry
|
||||
*/
|
||||
export interface TimeSpendReport extends AttachedDoc {
|
||||
attachedTo: Ref<Issue>
|
||||
|
||||
employee: Ref<Employee> | null
|
||||
|
||||
date: Timestamp | null
|
||||
// Value in man days
|
||||
value: number
|
||||
|
||||
description: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -245,7 +269,9 @@ export default plugin(trackerId, {
|
||||
TypeIssuePriority: '' as Ref<Class<Type<IssuePriority>>>,
|
||||
TypeProjectStatus: '' as Ref<Class<Type<ProjectStatus>>>,
|
||||
Sprint: '' as Ref<Class<Sprint>>,
|
||||
TypeSprintStatus: '' as Ref<Class<Type<SprintStatus>>>
|
||||
TypeSprintStatus: '' as Ref<Class<Type<SprintStatus>>>,
|
||||
TimeSpendReport: '' as Ref<Class<TimeSpendReport>>,
|
||||
TypeReportedTime: '' as Ref<Class<Type<number>>>
|
||||
},
|
||||
ids: {
|
||||
NoParent: '' as Ref<Issue>
|
||||
@ -311,7 +337,10 @@ export default plugin(trackerId, {
|
||||
|
||||
CopyID: '' as Asset,
|
||||
CopyURL: '' as Asset,
|
||||
CopyBranch: '' as Asset
|
||||
CopyBranch: '' as Asset,
|
||||
|
||||
TimeReport: '' as Asset,
|
||||
Estimation: '' as Asset
|
||||
},
|
||||
category: {
|
||||
Other: '' as Ref<TagCategory>,
|
||||
|
@ -120,13 +120,17 @@ export function filterActions (
|
||||
const role = getCurrentAccount().role
|
||||
const clazz = hierarchy.getClass(doc._class)
|
||||
const ignoreActions = hierarchy.as(clazz, view.mixin.IgnoreActions)
|
||||
const ignore: Array<Ref<Action>> = ignoreActions?.actions ?? []
|
||||
const ignore: Array<Ref<Action>> = Array.from(ignoreActions?.actions ?? [])
|
||||
|
||||
// Collect ignores from parent
|
||||
hierarchy.getAncestors(clazz._id).forEach((cl) => {
|
||||
const ancestors = hierarchy.getAncestors(clazz._id)
|
||||
|
||||
for (const cl of ancestors) {
|
||||
const ignoreActions = hierarchy.as(hierarchy.getClass(cl), view.mixin.IgnoreActions)
|
||||
ignore.push(...(ignoreActions?.actions ?? []))
|
||||
})
|
||||
if (ignoreActions?.actions !== undefined) {
|
||||
ignore.push(...ignoreActions.actions)
|
||||
}
|
||||
}
|
||||
const overrideRemove: Array<Ref<Action>> = []
|
||||
for (const action of actions) {
|
||||
if (ignore.includes(action._id)) {
|
||||
|
@ -26,9 +26,6 @@
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let actions: Action[] = []
|
||||
let q = 0
|
||||
|
||||
addTxListener((tx) => {
|
||||
if (tx._class === core.class.TxRemoveDoc) {
|
||||
const docId = (tx as TxRemoveDoc<Doc>).objectId
|
||||
@ -42,16 +39,14 @@
|
||||
|
||||
let lastKey: KeyboardEvent | undefined
|
||||
|
||||
async function updateActions (
|
||||
async function getCurrentActions (
|
||||
context: {
|
||||
mode: ViewContextType
|
||||
application?: Ref<Doc>
|
||||
},
|
||||
focus: Doc | undefined | null,
|
||||
selection: Doc[]
|
||||
): Promise<void> {
|
||||
const t = ++q
|
||||
|
||||
): Promise<Action[]> {
|
||||
let docs: Doc | Doc[] = []
|
||||
if (selection.find((it) => it._id === focus?._id) === undefined && focus != null) {
|
||||
docs = focus
|
||||
@ -59,18 +54,13 @@
|
||||
docs = selection
|
||||
}
|
||||
|
||||
const r = await getContextActions(client, docs, context)
|
||||
if (t === q) {
|
||||
actions = r
|
||||
}
|
||||
return await getContextActions(client, docs, context)
|
||||
}
|
||||
|
||||
$: ctx = $contextStore[$contextStore.length - 1]
|
||||
$: mode = $contextStore[$contextStore.length - 1]?.mode
|
||||
$: application = $contextStore[$contextStore.length - 1]?.application
|
||||
$: if (ctx !== undefined) {
|
||||
updateActions({ mode: mode as ViewContextType, application: application }, $focusStore.focus, $selectionStore)
|
||||
}
|
||||
|
||||
function keyPrefix (key: KeyboardEvent): string {
|
||||
return (
|
||||
(key.altKey ? 'Alt + ' : '') +
|
||||
@ -114,7 +104,11 @@
|
||||
elm = prt
|
||||
}
|
||||
|
||||
let currentActions = actions
|
||||
let currentActions = await getCurrentActions(
|
||||
{ mode: mode as ViewContextType, application: application },
|
||||
$focusStore.focus,
|
||||
$selectionStore
|
||||
)
|
||||
|
||||
// For none we ignore all actions.
|
||||
if (ctx.mode === 'none') {
|
||||
|
@ -25,6 +25,7 @@
|
||||
export let maxWidth: string = '10rem'
|
||||
export let onChange: (value: number) => void
|
||||
export let kind: 'no-border' | 'link' = 'no-border'
|
||||
export let readonly = false
|
||||
|
||||
let shown: boolean = false
|
||||
|
||||
@ -37,7 +38,7 @@
|
||||
<div
|
||||
class="link-container"
|
||||
on:click={(ev) => {
|
||||
if (!shown) {
|
||||
if (!shown && !readonly) {
|
||||
showPopup(EditBoxPopup, { value, format: 'number' }, eventToHTMLElement(ev), (res) => {
|
||||
if (res !== undefined) {
|
||||
value = res
|
||||
@ -48,12 +49,18 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if value}
|
||||
{#if value !== undefined}
|
||||
<span class="overflow-label">{value}</span>
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if readonly}
|
||||
{#if value !== undefined}
|
||||
<span class="overflow-label">{value}</span>
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
{:else}
|
||||
<EditBox {placeholder} {maxWidth} bind:value format={'number'} {focus} on:change={_onchange} />
|
||||
{/if}
|
||||
|
@ -25,6 +25,7 @@
|
||||
export let maxWidth: string = '10rem'
|
||||
export let onChange: (value: string) => void
|
||||
export let kind: 'no-border' | 'link' = 'no-border'
|
||||
export let readonly = false
|
||||
|
||||
let shown: boolean = false
|
||||
|
||||
@ -37,7 +38,7 @@
|
||||
<div
|
||||
class="link-container"
|
||||
on:click={(ev) => {
|
||||
if (!shown) {
|
||||
if (!shown && !readonly) {
|
||||
showPopup(EditBoxPopup, { value }, eventToHTMLElement(ev), (res) => {
|
||||
if (res !== undefined) {
|
||||
value = res
|
||||
@ -54,6 +55,12 @@
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if readonly}
|
||||
{#if value}
|
||||
<span class="overflow-label">{value}</span>
|
||||
{:else}
|
||||
<span class="dark-color"><Label label={placeholder} /></span>
|
||||
{/if}
|
||||
{:else}
|
||||
<EditBox {placeholder} {maxWidth} bind:value {focus} on:change={_onchange} />
|
||||
{/if}
|
||||
|
@ -29,7 +29,7 @@
|
||||
showPopup,
|
||||
Spinner
|
||||
} from '@anticrm/ui'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
import { AttributeModel, BuildModelKey } from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { buildConfigLookup, buildModel, LoadingProps } from '../utils'
|
||||
import Menu from './Menu.svelte'
|
||||
@ -170,6 +170,17 @@
|
||||
}
|
||||
return props
|
||||
}
|
||||
function getValue (attribute: AttributeModel, object: Doc): any {
|
||||
if (attribute.castRequest) {
|
||||
return (
|
||||
getObjectValue(
|
||||
attribute.key.substring(attribute.castRequest.length + 1),
|
||||
client.getHierarchy().as(object, attribute.castRequest)
|
||||
) ?? ''
|
||||
)
|
||||
}
|
||||
return getObjectValue(attribute.key, object) ?? ''
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await buildModel({ client, _class, keys: config, lookup })}
|
||||
@ -267,7 +278,7 @@
|
||||
<div class="antiTable-cells__firstCell">
|
||||
<svelte:component
|
||||
this={attribute.presenter}
|
||||
value={getObjectValue(attribute.key, object) ?? ''}
|
||||
value={getValue(attribute, object) ?? ''}
|
||||
{...joinProps(attribute.collectionAttr, object, attribute.props)}
|
||||
/>
|
||||
<!-- <div
|
||||
@ -283,7 +294,7 @@
|
||||
<td>
|
||||
<svelte:component
|
||||
this={attribute.presenter}
|
||||
value={getObjectValue(attribute.key, object) ?? ''}
|
||||
value={getValue(attribute, object) ?? ''}
|
||||
{...joinProps(attribute.collectionAttr, object, attribute.props)}
|
||||
/>
|
||||
</td>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import presentation, { Card, createQuery, getAttributePresenterClass, getClient } from '@anticrm/presentation'
|
||||
import { BuildModelKey, Viewlet, ViewletPreference } from '@anticrm/view'
|
||||
import core, { ArrOf, Class, Doc, Lookup, Ref, Type } from '@anticrm/core'
|
||||
import core, { AnyAttribute, ArrOf, Class, Doc, Lookup, Ref, Type } from '@anticrm/core'
|
||||
import { Button, ToggleButton } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
@ -54,6 +54,7 @@
|
||||
enabled: boolean
|
||||
label: IntlString
|
||||
value: string | BuildModelKey
|
||||
_class: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
function getObjectConfig (_class: Ref<Class<Doc>>, param: string): AttributeConfig {
|
||||
@ -61,7 +62,8 @@
|
||||
return {
|
||||
value: param,
|
||||
label: clazz.label,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
_class
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,14 +78,16 @@
|
||||
result.push({
|
||||
value: param,
|
||||
enabled: true,
|
||||
label: getKeyLabel(viewlet.attachTo, param, lookup)
|
||||
label: getKeyLabel(viewlet.attachTo, param, lookup),
|
||||
_class: viewlet.attachTo
|
||||
})
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
value: param,
|
||||
label: param.label as IntlString,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
_class: viewlet.attachTo
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -100,33 +104,59 @@
|
||||
return name
|
||||
}
|
||||
|
||||
function processAttribute (attribute: AnyAttribute, result: AttributeConfig[], useMixinProxy = false): void {
|
||||
if (attribute.hidden === true || attribute.label === undefined) return
|
||||
if (viewlet.hiddenKeys?.includes(attribute.name)) return
|
||||
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) return
|
||||
const value = getValue(attribute.name, attribute.type)
|
||||
if (result.findIndex((p) => p.value === value) !== -1) return
|
||||
const typeClassId = getAttributePresenterClass(hierarchy, attribute).attrClass
|
||||
const typeClass = hierarchy.getClass(typeClassId)
|
||||
let presenter = hierarchy.as(typeClass, view.mixin.AttributePresenter).presenter
|
||||
let parent = typeClass.extends
|
||||
while (presenter === undefined && parent !== undefined) {
|
||||
const pclazz = hierarchy.getClass(parent)
|
||||
presenter = hierarchy.as(pclazz, view.mixin.AttributePresenter).presenter
|
||||
parent = pclazz.extends
|
||||
}
|
||||
if (presenter === undefined) return
|
||||
|
||||
if (useMixinProxy) {
|
||||
result.push({
|
||||
value: attribute.attributeOf + '.' + attribute.name,
|
||||
label: attribute.label,
|
||||
enabled: false,
|
||||
_class: attribute.attributeOf
|
||||
})
|
||||
} else {
|
||||
result.push({
|
||||
value,
|
||||
label: attribute.label,
|
||||
enabled: false,
|
||||
_class: attribute.attributeOf
|
||||
})
|
||||
}
|
||||
}
|
||||
function getConfig (viewlet: Viewlet, preference: ViewletPreference | undefined): AttributeConfig[] {
|
||||
const result = getBaseConfig(viewlet)
|
||||
|
||||
const allAttributes = hierarchy.getAllAttributes(viewlet.attachTo)
|
||||
|
||||
for (const [, attribute] of allAttributes) {
|
||||
if (attribute.hidden === true || attribute.label === undefined) continue
|
||||
if (viewlet.hiddenKeys?.includes(attribute.name)) continue
|
||||
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) continue
|
||||
const value = getValue(attribute.name, attribute.type)
|
||||
if (result.findIndex((p) => p.value === value) !== -1) continue
|
||||
const typeClassId = getAttributePresenterClass(hierarchy, attribute).attrClass
|
||||
const typeClass = hierarchy.getClass(typeClassId)
|
||||
let presenter = hierarchy.as(typeClass, view.mixin.AttributePresenter).presenter
|
||||
let parent = typeClass.extends
|
||||
while (presenter === undefined && parent !== undefined) {
|
||||
const pclazz = hierarchy.getClass(parent)
|
||||
presenter = hierarchy.as(pclazz, view.mixin.AttributePresenter).presenter
|
||||
parent = pclazz.extends
|
||||
}
|
||||
if (presenter === undefined) continue
|
||||
result.push({
|
||||
value,
|
||||
label: attribute.label,
|
||||
enabled: false
|
||||
})
|
||||
processAttribute(attribute, result)
|
||||
}
|
||||
|
||||
hierarchy
|
||||
.getDescendants(viewlet.attachTo)
|
||||
.filter((it) => hierarchy.isMixin(it))
|
||||
.forEach((it) =>
|
||||
hierarchy.getAllAttributes(it, viewlet.attachTo).forEach((attr) => {
|
||||
if (attr.isCustom === true) {
|
||||
processAttribute(attr, result, true)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return preference === undefined ? result : setStatus(result, preference)
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,8 @@ export default mergeIds(viewId, view, {
|
||||
ValueFilter: '' as AnyComponent,
|
||||
TimestampFilter: '' as AnyComponent,
|
||||
FilterTypePopup: '' as AnyComponent,
|
||||
ActionsPopup: '' as AnyComponent
|
||||
ActionsPopup: '' as AnyComponent,
|
||||
ProxyPresenter: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
MoveClass: '' as IntlString,
|
||||
|
@ -230,6 +230,22 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
|
||||
.map((key) => (typeof key === 'string' ? { key: key } : key))
|
||||
.map(async (key) => {
|
||||
try {
|
||||
// Check if it is a mixin attribute configuration
|
||||
const pos = key.key.lastIndexOf('.')
|
||||
if (pos !== -1) {
|
||||
const mixinName = key.key.substring(0, pos) as Ref<Class<Doc>>
|
||||
if (options.client.getHierarchy().isMixin(mixinName)) {
|
||||
const realKey = key.key.substring(pos + 1)
|
||||
const rkey = { ...key, key: realKey }
|
||||
return {
|
||||
...(await getPresenter(options.client, mixinName, rkey, rkey, options.lookup)),
|
||||
castRequest: mixinName,
|
||||
key: key.key,
|
||||
sortingKey: key.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await getPresenter(options.client, options._class, key, key, options.lookup)
|
||||
} catch (err: any) {
|
||||
if (options.ignoreMissing ?? false) {
|
||||
|
@ -357,6 +357,8 @@ export interface AttributeModel {
|
||||
|
||||
attribute?: AnyAttribute
|
||||
collectionAttr: boolean
|
||||
|
||||
castRequest?: Ref<Mixin<Doc>>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
import type { Doc, Ref, Tx, TxCollectionCUD, TxCreateDoc, TxRemoveDoc } from '@anticrm/core'
|
||||
import type { TriggerControl } from '@anticrm/server-core'
|
||||
import { extractTx } from '@anticrm/server-core'
|
||||
import type { Attachment } from '@anticrm/attachment'
|
||||
import attachment from '@anticrm/attachment'
|
||||
import core, { TxProcessor } from '@anticrm/core'
|
||||
@ -51,7 +50,7 @@ export async function OnAttachmentDelete (
|
||||
tx: Tx,
|
||||
{ findAll, hierarchy, fulltextFx, storageFx }: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (actualTx._class !== core.class.TxRemoveDoc) {
|
||||
return []
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import core, {
|
||||
TxProcessor
|
||||
} from '@anticrm/core'
|
||||
import gmail, { Message } from '@anticrm/gmail'
|
||||
import { extractTx, TriggerControl } from '@anticrm/server-core'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -54,7 +54,7 @@ export async function FindMessages (
|
||||
* @public
|
||||
*/
|
||||
export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (actualTx._class !== core.class.TxCreateDoc) {
|
||||
return []
|
||||
}
|
||||
|
@ -14,9 +14,9 @@
|
||||
//
|
||||
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import core, { Ref, SortingOrder, Tx, TxFactory, TxMixin, TxUpdateDoc } from '@anticrm/core'
|
||||
import core, { Ref, SortingOrder, Tx, TxFactory, TxMixin, TxProcessor, TxUpdateDoc } from '@anticrm/core'
|
||||
import hr, { Department, DepartmentMember, Staff } from '@anticrm/hr'
|
||||
import { extractTx, TriggerControl } from '@anticrm/server-core'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
|
||||
async function getOldDepartment (
|
||||
currentTx: TxMixin<Employee, Staff> | TxUpdateDoc<Employee>,
|
||||
@ -89,7 +89,7 @@ function getTxes (
|
||||
* @public
|
||||
*/
|
||||
export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (core.class.TxMixin !== actualTx._class) {
|
||||
return []
|
||||
}
|
||||
@ -134,7 +134,7 @@ export async function OnDepartmentStaff (tx: Tx, control: TriggerControl): Promi
|
||||
* @public
|
||||
*/
|
||||
export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (core.class.TxUpdateDoc !== actualTx._class) {
|
||||
return []
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ import notification, {
|
||||
} from '@anticrm/notification'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import type { TriggerControl } from '@anticrm/server-core'
|
||||
import { extractTx } from '@anticrm/server-core'
|
||||
import { createLastViewTx, getUpdateLastViewTx } from '@anticrm/server-notification'
|
||||
import view, { HTMLPresenter, TextPresenter } from '@anticrm/view'
|
||||
|
||||
@ -139,7 +138,7 @@ async function getUpdateLastViewTxes (
|
||||
* @public
|
||||
*/
|
||||
export async function UpdateLastView (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (![core.class.TxUpdateDoc, core.class.TxCreateDoc, core.class.TxMixin].includes(actualTx._class)) {
|
||||
return []
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import core, {
|
||||
TxProcessor,
|
||||
TxRemoveDoc
|
||||
} from '@anticrm/core'
|
||||
import { extractTx, TriggerControl } from '@anticrm/server-core'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
import tags, { TagElement, TagReference } from '@anticrm/tags'
|
||||
|
||||
/**
|
||||
@ -50,7 +50,7 @@ export async function TagElementRemove (
|
||||
* @public
|
||||
*/
|
||||
export async function onTagReference (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)
|
||||
const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)
|
||||
if (!isCreate && !isRemove) return []
|
||||
@ -68,7 +68,7 @@ export async function onTagReference (tx: Tx, control: TriggerControl): Promise<
|
||||
await control.findAll(core.class.TxCollectionCUD, { 'tx.objectId': ctx.objectId }, { limit: 1 })
|
||||
)[0]
|
||||
if (createTx !== undefined) {
|
||||
const actualCreateTx = extractTx(createTx)
|
||||
const actualCreateTx = TxProcessor.extractTx(createTx)
|
||||
const doc = TxProcessor.createDoc2Doc(actualCreateTx as TxCreateDoc<TagReference>)
|
||||
const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, tags.space.Tags, doc.tag, {
|
||||
$inc: { refCount: -1 }
|
||||
|
@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { Doc, Tx, TxUpdateDoc } from '@anticrm/core'
|
||||
import core, { Doc, Tx, TxProcessor, TxUpdateDoc } from '@anticrm/core'
|
||||
import login from '@anticrm/login'
|
||||
import { getMetadata } from '@anticrm/platform'
|
||||
import { extractTx, TriggerControl } from '@anticrm/server-core'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
import { getUpdateLastViewTx } from '@anticrm/server-notification'
|
||||
import task, { Issue, Task, taskId } from '@anticrm/task'
|
||||
import view from '@anticrm/view'
|
||||
@ -43,7 +43,7 @@ export function issueTextPresenter (doc: Doc): string {
|
||||
* @public
|
||||
*/
|
||||
export async function OnTaskUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (actualTx._class !== core.class.TxUpdateDoc) {
|
||||
return []
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import core, {
|
||||
TxCreateDoc,
|
||||
TxProcessor
|
||||
} from '@anticrm/core'
|
||||
import { extractTx, TriggerControl } from '@anticrm/server-core'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
import telegram, { TelegramMessage } from '@anticrm/telegram'
|
||||
|
||||
/**
|
||||
@ -54,7 +54,7 @@ export async function FindMessages (
|
||||
* @public
|
||||
*/
|
||||
export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (actualTx._class !== core.class.TxCreateDoc) {
|
||||
return []
|
||||
}
|
||||
|
@ -13,9 +13,18 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { DocumentUpdate, Ref, Tx, TxUpdateDoc } from '@anticrm/core'
|
||||
import { extractTx, TriggerControl } from '@anticrm/server-core'
|
||||
import tracker, { Issue } from '@anticrm/tracker'
|
||||
import core, {
|
||||
DocumentUpdate,
|
||||
Ref,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
} from '@anticrm/core'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
import tracker, { Issue, TimeSpendReport } from '@anticrm/tracker'
|
||||
|
||||
async function updateSubIssues (
|
||||
updateTx: TxUpdateDoc<Issue>,
|
||||
@ -34,16 +43,93 @@ async function updateSubIssues (
|
||||
* @public
|
||||
*/
|
||||
export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
|
||||
// Check TimeReport operations
|
||||
if (
|
||||
actualTx._class === core.class.TxCreateDoc ||
|
||||
actualTx._class === core.class.TxUpdateDoc ||
|
||||
actualTx._class === core.class.TxRemoveDoc
|
||||
) {
|
||||
const cud = actualTx as TxCUD<TimeSpendReport>
|
||||
if (cud.objectClass === tracker.class.TimeSpendReport) {
|
||||
return await doTimeReportUpdate(cud, tx, control)
|
||||
}
|
||||
}
|
||||
|
||||
if (actualTx._class !== core.class.TxUpdateDoc) {
|
||||
return []
|
||||
}
|
||||
|
||||
const updateTx = actualTx as TxUpdateDoc<Issue>
|
||||
if (!control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) {
|
||||
return []
|
||||
if (control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) {
|
||||
return await doIssueUpdate(updateTx, control)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
OnIssueUpdate
|
||||
}
|
||||
})
|
||||
|
||||
async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const parentTx = tx as TxCollectionCUD<Issue, TimeSpendReport>
|
||||
switch (cud._class) {
|
||||
case core.class.TxCreateDoc: {
|
||||
const ccud = cud as TxCreateDoc<TimeSpendReport>
|
||||
return [
|
||||
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
|
||||
$inc: { reportedTime: ccud.attributes.value }
|
||||
})
|
||||
]
|
||||
}
|
||||
case core.class.TxUpdateDoc: {
|
||||
const upd = cud as TxUpdateDoc<TimeSpendReport>
|
||||
if (upd.operations.value !== undefined) {
|
||||
const logTxes = Array.from(
|
||||
await control.findAll(core.class.TxCollectionCUD, {
|
||||
'tx.objectId': cud.objectId,
|
||||
_id: { $nin: [parentTx._id] }
|
||||
})
|
||||
).map(TxProcessor.extractTx)
|
||||
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
|
||||
if (doc !== undefined) {
|
||||
return [
|
||||
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 }
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case core.class.TxRemoveDoc: {
|
||||
const logTxes = Array.from(
|
||||
await control.findAll(core.class.TxCollectionCUD, {
|
||||
'tx.objectId': cud.objectId,
|
||||
_id: { $nin: [parentTx._id] }
|
||||
})
|
||||
).map(TxProcessor.extractTx)
|
||||
const doc: TimeSpendReport | undefined = TxProcessor.buildDoc2Doc(logTxes)
|
||||
if (doc !== undefined) {
|
||||
return [
|
||||
control.txFactory.createTxUpdateDoc<Issue>(parentTx.objectClass, parentTx.objectSpace, parentTx.objectId, {
|
||||
$inc: { reportedTime: -1 * doc.value }
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerControl): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'attachedTo')) {
|
||||
@ -95,10 +181,3 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
OnIssueUpdate
|
||||
}
|
||||
})
|
||||
|
@ -19,5 +19,4 @@ export * from './types'
|
||||
export * from './fulltext'
|
||||
export * from './storage'
|
||||
export * from './pipeline'
|
||||
export * from './utils'
|
||||
export { default } from './plugin'
|
||||
|
@ -54,7 +54,6 @@ import { FullTextIndex } from './fulltext'
|
||||
import serverCore from './plugin'
|
||||
import { Triggers } from './triggers'
|
||||
import type { FullTextAdapter, FullTextAdapterFactory, ObjectDDParticipant } from './types'
|
||||
import { extractTx } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -298,7 +297,7 @@ class TServerStorage implements ServerStorage {
|
||||
}
|
||||
|
||||
async processRemove (ctx: MeasureContext, tx: Tx): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (!this.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)) return []
|
||||
const rtx = actualTx as TxRemoveDoc<Doc>
|
||||
const result: Tx[] = []
|
||||
@ -375,7 +374,7 @@ class TServerStorage implements ServerStorage {
|
||||
}
|
||||
|
||||
async processMove (ctx: MeasureContext, tx: Tx): Promise<Tx[]> {
|
||||
const actualTx = extractTx(tx)
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (!this.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return []
|
||||
const rtx = actualTx as TxUpdateDoc<Doc>
|
||||
if (rtx.operations.space === undefined || rtx.operations.space === rtx.objectSpace) return []
|
||||
|
@ -1,20 +0,0 @@
|
||||
import core, { AttachedDoc, Doc, Tx, TxCollectionCUD, TxCreateDoc } from '@anticrm/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function extractTx (tx: Tx): Tx {
|
||||
if (tx._class === core.class.TxCollectionCUD) {
|
||||
const ctx = tx as TxCollectionCUD<Doc, AttachedDoc>
|
||||
if (ctx.tx._class === core.class.TxCreateDoc) {
|
||||
const create = ctx.tx as TxCreateDoc<AttachedDoc>
|
||||
create.attributes.attachedTo = ctx.objectId
|
||||
create.attributes.attachedToClass = ctx.objectClass
|
||||
create.attributes.collection = ctx.collection
|
||||
return create
|
||||
}
|
||||
return ctx.tx
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
@ -78,12 +78,12 @@ describe('server', () => {
|
||||
clean: async (domain: Domain, docs: Ref<Doc>[]) => {}
|
||||
}),
|
||||
(token, pipeline, broadcast) => new ClientSession(broadcast, token, pipeline),
|
||||
3333
|
||||
3335
|
||||
)
|
||||
|
||||
function connect (): WebSocket {
|
||||
const token: string = generateToken('', 'latest')
|
||||
return new WebSocket(`ws://localhost:3333/${token}`)
|
||||
return new WebSocket(`ws://localhost:3335/${token}`)
|
||||
}
|
||||
|
||||
it('should connect to server', (done) => {
|
||||
@ -97,7 +97,7 @@ describe('server', () => {
|
||||
})
|
||||
|
||||
it('should not connect to server without token', (done) => {
|
||||
const conn = new WebSocket('ws://localhost:3333/xyz')
|
||||
const conn = new WebSocket('ws://localhost:3335/xyz')
|
||||
conn.on('error', () => {
|
||||
conn.close()
|
||||
})
|
||||
|
@ -32,8 +32,8 @@ test.describe('project tests', () => {
|
||||
await page.click(`.selectPopup button:has-text("${prjId}")`)
|
||||
await page.click('form button:has-text("Save issue")')
|
||||
await page.waitForSelector('form.antiCard', { state: 'detached' })
|
||||
await page.click(`.gridElement button:has-text("${prjId}")`)
|
||||
await page.click('.selectPopup button:has-text("No project")')
|
||||
|
||||
await page.click('.listGrid :has-text("issue")')
|
||||
})
|
||||
|
||||
test('create-project-with-status', async ({ page }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user