[UBER-150] Milestones update (#3212)

Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@icloud.com>
This commit is contained in:
Sergei Ogorelkov 2023-05-19 12:20:12 +04:00 committed by GitHub
parent 88d62bcff5
commit ffc3999bdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 70 additions and 381 deletions

View File

@ -390,12 +390,6 @@ export class TMilestone extends TDoc implements Milestone {
@Index(IndexKind.Indexed)
status!: MilestoneStatus
@Prop(TypeRef(contact.class.Employee), tracker.string.ComponentLead)
lead!: Ref<Employee> | null
@Prop(ArrOf(TypeRef(contact.class.Employee)), tracker.string.Members)
members!: Ref<Employee>[]
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments!: number
@ -406,9 +400,6 @@ export class TMilestone extends TDoc implements Milestone {
targetDate!: Timestamp
declare space: Ref<Project>
@Prop(TypeNumber(), tracker.string.Capacity)
capacity!: number
}
/**
@ -955,6 +946,10 @@ export function createModel (builder: Builder): void {
presenter: tracker.component.MilestoneRefPresenter
})
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectEditor, {
editor: tracker.component.EditMilestone
})
builder.mixin(tracker.class.Issue, core.class.Class, setting.mixin.Editable, {
value: true
})
@ -1336,9 +1331,6 @@ export function createModel (builder: Builder): void {
filters: []
})
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ClassFilters, {
filters: []
})
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ClassFilters, {
filters: ['status'],
strict: true
@ -1766,11 +1758,10 @@ export function createModel (builder: Builder): void {
)
const milestoneOptions: ViewOptionsModel = {
groupBy: ['lead'],
groupBy: ['status'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['targetDate', SortingOrder.Descending],
['capacity', SortingOrder.Ascending]
['targetDate', SortingOrder.Descending]
],
other: []
}
@ -1789,26 +1780,7 @@ export function createModel (builder: Builder): void {
},
{ key: '', presenter: tracker.component.MilestonePresenter, props: { shouldUseMargin: true } },
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.MilestoneMembersTitle,
intlSearchPh: tracker.string.MilestoneMembersSearchPlaceholder
}
},
{ key: '', presenter: tracker.component.MilestoneDatePresenter, props: { field: 'targetDate' } },
{
key: 'lead',
presenter: tracker.component.MilestoneLeadPresenter,
props: {
_class: tracker.class.Milestone,
defaultClass: contact.class.Employee,
shouldShowLabel: false,
size: 'x-small'
}
}
{ key: '', presenter: tracker.component.MilestoneDatePresenter, props: { field: 'targetDate' } }
]
},
tracker.viewlet.MilestoneList
@ -1857,32 +1829,6 @@ export function createModel (builder: Builder): void {
['comments', 'status', 'priority', 'assignee', 'subIssues', 'blockedBy', 'milestone', 'dueDate']
)
createAction(
builder,
{
action: view.actionImpl.ValueSelector,
actionPopup: view.component.ValueSelector,
actionProps: {
attribute: 'lead',
_class: contact.class.Employee,
query: {},
placeholder: tracker.string.MilestoneLead
},
label: tracker.string.MilestoneLead,
icon: contact.icon.Person,
keyBinding: [],
input: 'none',
category: tracker.category.Tracker,
target: tracker.class.Milestone,
context: {
mode: ['context'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetMilestoneLead
)
const componentListViewOptions: ViewOptionsModel = {
groupBy: ['lead'],
orderBy: [

View File

@ -86,7 +86,6 @@ export default mergeIds(trackerId, tracker, {
action: {
NewRelatedIssue: '' as Ref<Action<Doc, Record<string, any>>>,
DeleteMilestone: '' as Ref<Action<Doc, Record<string, any>>>,
DeleteProject: '' as Ref<Action<Doc, Record<string, any>>>,
SetMilestoneLead: '' as Ref<Action<Doc, Record<string, any>>>
DeleteProject: '' as Ref<Action<Doc, Record<string, any>>>
}
})

View File

@ -207,11 +207,6 @@
"ClosedMilestones": "Done",
"AddToMilestone": "Add to Milestone",
"MilestoneNamePlaceholder": "Milestone name",
"MilestoneLead": "Lead",
"MilestoneLeadTitle": "Milestone lead",
"MilestoneLeadSearchPlaceholder": "Set milestone lead\u2026",
"MilestoneMembersTitle": "Milestone members",
"MilestoneMembersSearchPlaceholder": "Change milestone members\u2026",
"NewMilestone": "New Milestone",
"CreateMilestone": "Create",
@ -252,7 +247,6 @@
"TimeSpendHours": "{value}h",
"ChildEstimation": "Subissues Estimation",
"ChildReportedTime": "Subissues Time",
"Capacity": "Capacity",
"CapacityValue": "of {value}d",
"NewRelatedIssue": "New related issue",
"RelatedIssuesNotFound": "Related issues not found",

View File

@ -207,11 +207,6 @@
"ClosedMilestones": "Завершено",
"AddToMilestone": "Добавить в Майлстоун",
"MilestoneNamePlaceholder": "Название майлстоуна",
"MilestoneLead": "Руководитель",
"MilestoneLeadTitle": "Руководитель майлстоуна",
"MilestoneLeadSearchPlaceholder": "Назначьте руководителя майлстоуна\u2026",
"MilestoneMembersTitle": "Участники майлстоуна",
"MilestoneMembersSearchPlaceholder": "Измененить участников майлстоуна\u2026",
"NewMilestone": "Новый Майлстоун",
"CreateMilestone": "Создать",
@ -252,7 +247,6 @@
"TimeSpendHours": "{value}h",
"ChildEstimation": "Оценка подзадач",
"ChildReportedTime": "Время водзадач",
"Capacity": "Вместимость",
"CapacityValue": "из {value}d",
"NewRelatedIssue": "Завести связанную задачу",
"RelatedIssuesNotFound": "Связанные задачи не найдены",

View File

@ -1,89 +1,56 @@
<!--
// Copyright © 2023 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 '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Milestone } from '@hcengineering/tracker'
import { Button, DatePresenter, EditBox, Icon, IconMoreH, Label, showPopup } from '@hcengineering/ui'
import { ContextMenu, DocAttributeBar } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { onDestroy } from 'svelte'
import { activeMilestone } from '../../issues'
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import tracker from '../../plugin'
import Expanded from '../icons/Expanded.svelte'
import IssuesView from '../issues/IssuesView.svelte'
import MilestonePopup from './MilestonePopup.svelte'
export let milestone: Milestone
export let object: Milestone
const client = getClient()
const dispatch = createEventDispatcher()
const client = getClient()
async function change (field: string, value: any) {
await client.update(milestone, { [field]: value })
}
let container: HTMLElement
let oldLabel = ''
let rawLabel = ''
function selectMilestone (): void {
showPopup(MilestonePopup, { _class: tracker.class.Milestone }, container, (value) => {
if (value != null) {
milestone = value
dispatch('milestone', milestone._id)
}
})
async function change<K extends keyof Milestone> (field: K, value: Milestone[K]) {
await client.update(object, { [field]: value })
}
function showMenu (ev?: Event): void {
if (milestone) {
showPopup(ContextMenu, { object: milestone }, (ev as MouseEvent).target as HTMLElement)
}
$: if (oldLabel !== object.label) {
oldLabel = object.label
rawLabel = object.label
}
$: $activeMilestone = milestone?._id
onDestroy(() => {
$activeMilestone = undefined
})
onMount(() => dispatch('open', { ignoreKeys: ['label'] }))
</script>
<IssuesView
query={{ milestone: milestone._id, space: milestone.space }}
space={milestone.space}
label={milestone.label}
>
<svelte:fragment slot="label_selector">
<div bind:this={container}>
<Button size={'small'} kind={'link'} on:click={selectMilestone}>
<svelte:fragment slot="content">
<div class="ac-header__icon"><Icon icon={tracker.icon.Milestone} size={'small'} /></div>
<span class="ac-header__title mr-1">{milestone.label}</span>
<Icon icon={Expanded} size={'small'} />
</svelte:fragment>
</Button>
</div>
</svelte:fragment>
<svelte:fragment slot="afterHeader">
<div class="ac-header search-start full divide">
<DatePresenter value={milestone.targetDate} kind={'transparent'} size={'medium'} />
<div class="flex-row-center ml-2">
{#if milestone?.capacity}
<Label label={tracker.string.CapacityValue} params={{ value: milestone?.capacity }} />
{/if}
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="aside">
<div class="popupPanel-body__aside-content">
<EditBox kind={'large-style'} bind:value={milestone.label} on:change={() => change('label', milestone.label)} />
<div class="mt-2">
<StyledTextBox
alwaysEdit={true}
showButtons={false}
placeholder={tracker.string.Description}
content={milestone.description ?? ''}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
</div>
<DocAttributeBar object={milestone} mixins={[]} ignoreKeys={['icon', 'label', 'description']} />
</svelte:fragment>
</IssuesView>
<EditBox
bind:value={rawLabel}
placeholder={tracker.string.MilestoneNamePlaceholder}
kind="large-style"
focusable
on:blur={async () => {
const trimmedLabel = rawLabel.trim()
if (trimmedLabel.length === 0) {
rawLabel = oldLabel
} else if (trimmedLabel !== object.label) {
await change('label', trimmedLabel)
}
}}
/>

View File

@ -138,7 +138,7 @@
<SearchEdit bind:value={search} on:change={() => {}} />
<!-- <ActionIcon icon={IconMoreH} size={'small'} /> -->
<div class="buttons-divider" />
<FilterButton _class={tracker.class.Issue} {space} />
<FilterButton _class={tracker.class.Milestone} {space} />
</div>
<div class="ac-header-full medium-gap">
{#if viewlet}

View File

@ -1,9 +1,8 @@
<script lang="ts">
import contact from '@hcengineering/contact'
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Milestone } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import NewMilestone from './NewMilestone.svelte'
@ -16,19 +15,6 @@
const createItemDialog = NewMilestone
const createItemLabel = tracker.string.CreateMilestone
const retrieveMembers = (s: Milestone) => s.members
function updateConfig (config: (string | BuildModelKey)[]): (string | BuildModelKey)[] {
return config.map((it) => {
if (typeof it === 'string') {
return it
}
return it.presenter === contact.component.MembersPresenter
? { ...it, props: { ...it.props, retrieveMembers } }
: it
})
}
</script>
{#if viewlet?.$lookup?.descriptor?.component}
@ -36,7 +22,7 @@
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.Milestone,
config: updateConfig(viewlet.config),
config: viewlet.config,
options: viewlet.options,
createItemDialog,
createItemLabel,

View File

@ -1,106 +0,0 @@
<!--
// 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 '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { UsersPopup } from '@hcengineering/contact-resources'
import { Milestone } from '@hcengineering/tracker'
import { eventToHTMLElement, IconSize, showPopup } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import LeadPopup from '../components/LeadPopup.svelte'
export let value: Employee | null
export let size: IconSize = 'x-small'
export let object: Milestone
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false
export let defaultName: IntlString | undefined = undefined
const client = getClient()
let presenter: AttributeModel | undefined
$: if (value || defaultClass) {
if (value) {
getObjectPresenter(client, value._class, { key: '' }).then((p) => {
presenter = p
})
} else if (defaultClass) {
getObjectPresenter(client, defaultClass, { key: '' }).then((p) => {
presenter = p
})
}
}
const handleLeadChanged = async (result: Employee | null | undefined) => {
if (!isEditable || result === undefined) {
return
}
const newLead = result === null ? null : result._id
await client.update(object, { lead: newLead })
}
const handleLeadEditorOpened = async (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(
UsersPopup,
{
_class: contact.class.Employee,
selected: value?._id,
docQuery: {
active: true
},
allowDeselect: true,
placeholder: tracker.string.ComponentLeadSearchPlaceholder
},
eventToHTMLElement(event),
handleLeadChanged
)
}
</script>
{#if value && presenter}
<svelte:component
this={presenter.presenter}
{value}
{defaultName}
avatarSize={size}
disabled={false}
shouldShowPlaceholder={true}
shouldShowName={shouldShowLabel}
onEmployeeEdit={handleLeadEditorOpened}
tooltipLabels={{ component: LeadPopup, props: { lead: value } }}
/>
{:else if presenter}
<svelte:component
this={presenter.presenter}
{value}
{defaultName}
avatarSize={size}
disabled={false}
shouldShowPlaceholder={true}
shouldShowName={shouldShowLabel}
onEmployeeEdit={handleLeadEditorOpened}
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
/>
{/if}

View File

@ -15,42 +15,26 @@
<script lang="ts">
import { WithLookup } from '@hcengineering/core'
import { Milestone } from '@hcengineering/tracker'
import { Icon, getCurrentResolvedLocation, navigate, tooltip } from '@hcengineering/ui'
import { Icon } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import tracker from '../../plugin'
export let value: WithLookup<Milestone>
export let shouldShowAvatar: boolean = true
export let onClick: (() => void) | undefined = undefined
export let shouldShowAvatar = true
export let disabled = false
export let inline: boolean = false
function navigateToMilestone () {
if (disabled) {
return
}
if (onClick) {
onClick()
}
const loc = getCurrentResolvedLocation()
loc.path[4] = 'milestones'
loc.path[5] = value._id
loc.path.length = 6
loc.fragment = undefined
navigate(loc)
}
export let inline = false
export let onClick: (() => void) | undefined = undefined
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter" class:inline-presenter={inline} on:click={navigateToMilestone}>
<DocNavLink object={value} {disabled} {inline} {onClick}>
<div class="flex-presenter" class:inline-presenter={inline}>
{#if !inline && shouldShowAvatar}
<div class="icon" use:tooltip={{ label: tracker.string.Milestone }}>
<Icon icon={tracker.icon.Milestone} size={'small'} />
<div class="icon">
<Icon icon={tracker.icon.Milestone} size="small" />
</div>
{/if}
<span title={value.label} class="overflow-label label">
{value.label}
</span>
</div>
{/if}
</DocNavLink>

View File

@ -15,58 +15,15 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Milestone, Project } from '@hcengineering/tracker'
import {
closePopup,
closeTooltip,
getCurrentResolvedLocation,
navigate,
resolvedLocationStore
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { Project } from '@hcengineering/tracker'
import tracker from '../../plugin'
import { MilestoneViewMode } from '../../utils'
import EditMilestone from './EditMilestone.svelte'
import MilestoneBrowser from './MilestoneBrowser.svelte'
export let currentSpace: Ref<Project>
export let label: IntlString = tracker.string.Milestones
export let search: string = ''
export let mode: MilestoneViewMode = 'all'
let milestoneId: Ref<Milestone> | undefined
let milestone: Milestone | undefined
onDestroy(
resolvedLocationStore.subscribe(async (loc) => {
closeTooltip()
closePopup()
milestoneId = loc.path[5] as Ref<Milestone>
})
)
const milestoneQuery = createQuery()
$: if (milestoneId !== undefined) {
milestoneQuery.query(tracker.class.Milestone, { _id: milestoneId }, (result) => {
milestone = result.shift()
})
} else {
milestoneQuery.unsubscribe()
milestone = undefined
}
</script>
{#if milestone}
<EditMilestone
{milestone}
on:milestone={(evt) => {
const loc = getCurrentResolvedLocation()
loc.path[5] = evt.detail
navigate(loc)
}}
/>
{:else}
<MilestoneBrowser {label} query={{ space: currentSpace }} {search} {mode} />
{/if}
<MilestoneBrowser {label} query={{ space: currentSpace }} {search} {mode} />

View File

@ -16,13 +16,12 @@
import { Data, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Card, getClient, SpaceSelector } from '@hcengineering/presentation'
import { EmployeeBox, UserBoxList } from '@hcengineering/contact-resources'
import { Milestone, MilestoneStatus, Project } from '@hcengineering/tracker'
import ui, { DatePresenter, EditBox } from '@hcengineering/ui'
import { StyledTextArea } from '@hcengineering/text-editor'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import MilestoneStatusSelector from './MilestoneStatusSelector.svelte'
import { StyledTextArea } from '@hcengineering/text-editor'
export let space: Ref<Project>
const dispatch = createEventDispatcher()
@ -32,11 +31,8 @@
label: '' as IntlString,
description: '',
status: MilestoneStatus.Planned,
lead: null,
members: [],
comments: 0,
attachments: 0,
capacity: 0,
targetDate: Date.now() + 14 * 24 * 60 * 60 * 1000
}
@ -83,22 +79,6 @@
kind={'secondary'}
size={'large'}
/>
<EmployeeBox
label={tracker.string.MilestoneLead}
placeholder={tracker.string.AssignTo}
kind={'secondary'}
size={'large'}
bind:value={object.lead}
allowDeselect
titleDeselect={tracker.string.Unassigned}
showNavigate={false}
/>
<UserBoxList
bind:items={object.members}
label={tracker.string.MilestoneMembersSearchPlaceholder}
kind={'secondary'}
size={'large'}
/>
<DatePresenter
bind:value={object.targetDate}
editable

View File

@ -15,7 +15,7 @@
<script lang="ts">
import contact from '@hcengineering/contact'
import { Ref, SortingOrder } from '@hcengineering/core'
import { ScrumRecord, Milestone, Project } from '@hcengineering/tracker'
import { ScrumRecord, Project, Scrum } from '@hcengineering/tracker'
import { Button, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { ActionContext, List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
@ -31,7 +31,7 @@
showPopup(NewScrum, { space: currentSpace, targetElement: null }, null)
}
const retrieveMembers = (s: Milestone) => s.members
const retrieveMembers = (s: Scrum) => s.members
</script>
<ActionContext

View File

@ -69,7 +69,7 @@ import RelationsPopup from './components/RelationsPopup.svelte'
import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
import MilestoneDatePresenter from './components/milestones/MilestoneDatePresenter.svelte'
import MilestoneLeadPresenter from './components/milestones/MilestoneLeadPresenter.svelte'
import EditMilestone from './components/milestones/EditMilestone.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
import Views from './components/views/Views.svelte'
import Statuses from './components/workflow/Statuses.svelte'
@ -423,6 +423,7 @@ export default async (): Promise<Resources> => ({
CreateIssueTemplate,
Milestones,
MilestonePresenter,
EditMilestone,
Scrums,
ScrumRecordPanel,
MilestoneStatusPresenter,
@ -450,7 +451,6 @@ export default async (): Promise<Resources> => ({
DeleteComponentPresenter,
TimeSpendReportPopup,
MilestoneDatePresenter,
MilestoneLeadPresenter,
NotificationIssuePresenter,
MilestoneFilter,
PriorityFilterValuePresenter,

View File

@ -229,11 +229,6 @@ export default mergeIds(trackerId, tracker, {
ActiveMilestones: '' as IntlString,
ClosedMilestones: '' as IntlString,
MilestoneNamePlaceholder: '' as IntlString,
MilestoneLead: '' as IntlString,
MilestoneLeadTitle: '' as IntlString,
MilestoneLeadSearchPlaceholder: '' as IntlString,
MilestoneMembersTitle: '' as IntlString,
MilestoneMembersSearchPlaceholder: '' as IntlString,
NewMilestone: '' as IntlString,
CreateMilestone: '' as IntlString,
@ -277,7 +272,6 @@ export default mergeIds(trackerId, tracker, {
ChildEstimation: '' as IntlString,
ChildReportedTime: '' as IntlString,
Capacity: '' as IntlString,
CapacityValue: '' as IntlString,
AddedReference: '' as IntlString,
AddedAsBlocked: '' as IntlString,
@ -360,7 +354,7 @@ export default mergeIds(trackerId, tracker, {
MilestoneStatusPresenter: '' as AnyComponent,
MilestoneTitlePresenter: '' as AnyComponent,
MilestoneDatePresenter: '' as AnyComponent,
MilestoneLeadPresenter: '' as AnyComponent,
EditMilestone: '' as AnyComponent,
ReportedTimeEditor: '' as AnyComponent,
TimeSpendReport: '' as AnyComponent,
EstimationEditor: '' as AnyComponent,

View File

@ -124,18 +124,12 @@ export interface Milestone extends Doc {
status: MilestoneStatus
lead: Ref<Employee> | null
members: Ref<Employee>[]
space: Ref<Project>
comments: number
attachments?: number
targetDate: Timestamp
// Capacity in man days.
capacity: number
}
/**