mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 03:40:48 +00:00
Sprints List and Reports view (#2602)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
9c9b3091dc
commit
769c9bd341
@ -1241,6 +1241,10 @@ export function createModel (builder: Builder): void {
|
||||
filters: ['priority', 'assignee', 'project', 'sprint', 'modifiedOn']
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['status', 'project', 'lead', 'startDate', 'targetDate', 'modifiedOn', 'capacity']
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
presentation.class.ObjectSearchCategory,
|
||||
core.space.Model,
|
||||
@ -1600,4 +1604,78 @@ export function createModel (builder: Builder): void {
|
||||
view.component.NumberPresenter,
|
||||
tracker.component.ReportedTimeEditor
|
||||
)
|
||||
|
||||
const sprintOptions: ViewOptionsModel = {
|
||||
groupBy: ['project', 'lead'],
|
||||
orderBy: [
|
||||
['startDate', SortingOrder.Descending],
|
||||
['modifiedOn', SortingOrder.Descending],
|
||||
['targetDate', SortingOrder.Descending],
|
||||
['capacity', SortingOrder.Ascending]
|
||||
],
|
||||
other: []
|
||||
}
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: tracker.class.Sprint,
|
||||
descriptor: view.viewlet.List,
|
||||
viewOptions: sprintOptions,
|
||||
config: [
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.SprintStatusPresenter,
|
||||
props: { width: '1rem', kind: 'list', size: 'small', justify: 'center' }
|
||||
},
|
||||
{ key: '', presenter: tracker.component.SprintPresenter, props: { shouldUseMargin: true } },
|
||||
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
|
||||
{ key: '', presenter: tracker.component.SprintProjectEditor, props: { kind: 'list' } },
|
||||
{
|
||||
key: '',
|
||||
presenter: contact.component.MembersPresenter,
|
||||
props: {
|
||||
kind: 'link',
|
||||
intlTitle: tracker.string.SprintMembersTitle,
|
||||
intlSearchPh: tracker.string.SprintMembersSearchPlaceholder
|
||||
}
|
||||
},
|
||||
{ key: '', presenter: tracker.component.SprintDatePresenter, props: { field: 'startDate' } },
|
||||
{ key: '', presenter: tracker.component.SprintDatePresenter, props: { field: 'targetDate' } },
|
||||
{
|
||||
key: '$lookup.lead',
|
||||
presenter: tracker.component.SprintLeadPresenter,
|
||||
props: {
|
||||
_class: tracker.class.Sprint,
|
||||
defaultClass: contact.class.Employee,
|
||||
shouldShowLabel: false,
|
||||
size: 'x-small'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: view.actionImpl.ValueSelector,
|
||||
actionPopup: view.component.ValueSelector,
|
||||
actionProps: {
|
||||
attribute: 'lead',
|
||||
_class: contact.class.Employee,
|
||||
query: {},
|
||||
placeholder: tracker.string.SprintLead
|
||||
},
|
||||
label: tracker.string.SprintLead,
|
||||
icon: contact.icon.Person,
|
||||
keyBinding: [],
|
||||
input: 'none',
|
||||
category: tracker.category.Tracker,
|
||||
target: tracker.class.Sprint,
|
||||
context: {
|
||||
mode: ['context'],
|
||||
application: tracker.app.Tracker,
|
||||
group: 'edit'
|
||||
}
|
||||
},
|
||||
tracker.action.SetSprintLead
|
||||
)
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
},
|
||||
action: {
|
||||
NewRelatedIssue: '' as Ref<Action<Doc, Record<string, any>>>,
|
||||
DeleteSprint: '' as Ref<Action<Doc, Record<string, any>>>
|
||||
DeleteSprint: '' as Ref<Action<Doc, Record<string, any>>>,
|
||||
SetSprintLead: '' as Ref<Action<Doc, Record<string, any>>>
|
||||
}
|
||||
})
|
||||
|
@ -51,6 +51,7 @@
|
||||
{#each requests as request}
|
||||
{#await getType(request) then type}
|
||||
{#if type}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="request flex-center"
|
||||
class:cursor-pointer={editable}
|
||||
|
@ -148,7 +148,7 @@
|
||||
|
||||
const reportQuery = createQuery()
|
||||
|
||||
import tracker from '@hcengineering/tracker'
|
||||
import tracker, { Issue } from '@hcengineering/tracker'
|
||||
import { EmployeeReports, fromTzDate, getEndDate, getStartDate } from '../utils'
|
||||
|
||||
let timeReports: Map<Ref<Employee>, EmployeeReports> = new Map()
|
||||
@ -163,17 +163,24 @@
|
||||
const newMap = new Map<Ref<Employee>, EmployeeReports>()
|
||||
for (const r of res) {
|
||||
if (r.employee != null) {
|
||||
const or = newMap.get(r.employee)
|
||||
newMap.set(r.employee, { value: (or?.value ?? 0) + r.value, reports: [...(or?.reports ?? []), r] })
|
||||
const or = newMap.get(r.employee) ?? {
|
||||
value: 0,
|
||||
reports: [],
|
||||
tasks: new Map()
|
||||
}
|
||||
const tsk = r.$lookup?.attachedTo as Issue
|
||||
newMap.set(r.employee, {
|
||||
value: or.value + r.value,
|
||||
reports: [...or.reports, r],
|
||||
tasks: or.tasks.set(tsk._id, tsk)
|
||||
})
|
||||
}
|
||||
}
|
||||
timeReports = newMap
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
_id: {
|
||||
attachedTo: tracker.class.Issue
|
||||
}
|
||||
attachedTo: tracker.class.Issue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -83,6 +83,18 @@
|
||||
function getOverrideConfig (startDate: Date): Map<string, BuildModelKey> {
|
||||
const typevals = getTypeVals(startDate)
|
||||
const endDate = getEndDate(startDate.getFullYear(), startDate.getMonth())
|
||||
|
||||
const getReport = (id: Ref<Doc>): EmployeeReports => {
|
||||
return timeReports.get(id as Ref<Employee>) ?? { value: 0, reports: [], tasks: new Map() }
|
||||
}
|
||||
const getTPD = (id: Ref<Doc>): number => {
|
||||
const rr = getReport(id)
|
||||
if (rr.value === 0) {
|
||||
return 0
|
||||
}
|
||||
return rr.tasks.size / rr.value
|
||||
}
|
||||
|
||||
return new Map<string, BuildModelKey>([
|
||||
[
|
||||
'@wdCount',
|
||||
@ -109,12 +121,38 @@
|
||||
presenter: ReportPresenter,
|
||||
props: {
|
||||
month: startDate ?? getStartDate(currentDate.getFullYear(), currentDate.getMonth()),
|
||||
display: (staff: Staff) => (timeReports.get(staff._id) ?? { value: 0 }).value
|
||||
display: (staff: Staff) => getReport(staff._id).value
|
||||
},
|
||||
sortingKey: '@wdCount',
|
||||
sortingFunction: (a: Doc, b: Doc) =>
|
||||
getTotal(getStatRequests(b._id as Ref<Staff>, startDate), startDate, endDate, types) -
|
||||
getTotal(getStatRequests(a._id as Ref<Staff>, startDate), startDate, endDate, types)
|
||||
sortingKey: '@wdCountReported',
|
||||
sortingFunction: (a: Doc, b: Doc) => getReport(b._id).value - getReport(a._id).value
|
||||
}
|
||||
],
|
||||
[
|
||||
'@wdTaskCountReported',
|
||||
{
|
||||
key: '',
|
||||
label: getEmbeddedLabel('Tasks'),
|
||||
presenter: ReportPresenter,
|
||||
props: {
|
||||
month: startDate ?? getStartDate(currentDate.getFullYear(), currentDate.getMonth()),
|
||||
display: (staff: Staff) => getReport(staff._id).tasks.size
|
||||
},
|
||||
sortingKey: '@wdTaskCountReported',
|
||||
sortingFunction: (a: Doc, b: Doc) => getReport(b._id).tasks.size - getReport(a._id).tasks.size
|
||||
}
|
||||
],
|
||||
[
|
||||
'@wdTaskPerDayReported',
|
||||
{
|
||||
key: '',
|
||||
label: getEmbeddedLabel('TPD'),
|
||||
presenter: ReportPresenter,
|
||||
props: {
|
||||
month: startDate ?? getStartDate(currentDate.getFullYear(), currentDate.getMonth()),
|
||||
display: (staff: Staff) => getTPD(staff._id)
|
||||
},
|
||||
sortingKey: '@wdTaskPerDayReported',
|
||||
sortingFunction: (a: Doc, b: Doc) => getTPD(b._id) - getTPD(a._id)
|
||||
}
|
||||
],
|
||||
[
|
||||
|
@ -39,6 +39,7 @@
|
||||
import CreateRequest from '../CreateRequest.svelte'
|
||||
import RequestsPopup from '../RequestsPopup.svelte'
|
||||
import ScheduleRequests from '../ScheduleRequests.svelte'
|
||||
import ReportsPopup from './ReportsPopup.svelte'
|
||||
|
||||
export let currentDate: Date = new Date()
|
||||
|
||||
@ -106,6 +107,13 @@
|
||||
bottom: 3.5
|
||||
}
|
||||
}
|
||||
|
||||
function showReportInfo (employee: Staff, rTime: EmployeeReports | undefined): void {
|
||||
if (rTime === undefined) {
|
||||
return
|
||||
}
|
||||
showPopup(ReportsPopup, { employee, reports: rTime.reports }, 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if departmentStaff.length}
|
||||
@ -152,9 +160,14 @@
|
||||
>
|
||||
{getTotal(requests, startDate, endDate, types)}
|
||||
</td>
|
||||
<td class="p-1 text-center">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<td
|
||||
class="p-1 text-center whitespace-nowrap cursor-pointer"
|
||||
on:click={() => showReportInfo(employee, rTime)}
|
||||
>
|
||||
{#if rTime !== undefined}
|
||||
{floorFractionDigits(rTime.value, 3)}
|
||||
({rTime.tasks.size})
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
@ -166,6 +179,7 @@
|
||||
{@const tooltipValue = getTooltip(requests)}
|
||||
{@const ww = findReports(employee, day, timeReports)}
|
||||
{#key [tooltipValue, editable]}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<td
|
||||
class="w-9 max-w-9 min-w-9"
|
||||
class:today={areDatesEqual(todayDate, day)}
|
||||
|
@ -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, { Employee } from '@hcengineering/contact'
|
||||
import { EmployeePresenter } from '@hcengineering/contact-resources'
|
||||
import { FindOptions } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import presentation, { Card } from '@hcengineering/presentation'
|
||||
import tracker, { TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { TableBrowser } from '@hcengineering/view-resources'
|
||||
|
||||
export let reports: TimeSpendReport[]
|
||||
export let employee: Employee
|
||||
|
||||
export function canClose (): boolean {
|
||||
return true
|
||||
}
|
||||
const options: FindOptions<TimeSpendReport> = {
|
||||
lookup: {
|
||||
attachedTo: tracker.class.Issue,
|
||||
employee: contact.class.Employee
|
||||
},
|
||||
sort: {
|
||||
date: -1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={getEmbeddedLabel('Reports')}
|
||||
canSave={true}
|
||||
on:close
|
||||
on:changeContent
|
||||
okAction={() => {}}
|
||||
okLabel={presentation.string.Ok}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<EmployeePresenter value={employee} disableClick />
|
||||
</svelte:fragment>
|
||||
<TableBrowser
|
||||
showFilterBar={false}
|
||||
_class={tracker.class.TimeSpendReport}
|
||||
query={{ _id: { $in: reports.map((it) => it._id) } }}
|
||||
config={['$lookup.attachedTo', '$lookup.attachedTo.title', '', 'employee', 'date']}
|
||||
{options}
|
||||
/>
|
||||
</Card>
|
@ -2,7 +2,7 @@ import { Employee, formatName } from '@hcengineering/contact'
|
||||
import { Ref, TxOperations } from '@hcengineering/core'
|
||||
import { Department, Request, RequestType, Staff, TzDate } from '@hcengineering/hr'
|
||||
import { MessageBox } from '@hcengineering/presentation'
|
||||
import { TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { isWeekend, MILLISECONDS_IN_DAY, showPopup } from '@hcengineering/ui'
|
||||
import hr from './plugin'
|
||||
|
||||
@ -235,5 +235,6 @@ export function tableToCSV (tableId: string, separator = ','): string {
|
||||
|
||||
export interface EmployeeReports {
|
||||
reports: TimeSpendReport[]
|
||||
tasks: Map<Ref<Issue>, Issue>
|
||||
value: number
|
||||
}
|
||||
|
@ -66,7 +66,7 @@
|
||||
<TableBrowser
|
||||
showFilterBar={false}
|
||||
_class={tracker.class.TimeSpendReport}
|
||||
query={{ attachedTo: { $in: [issue._id, ...issue.childInfo.map((it) => it.childId)] } }}
|
||||
query={{ attachedTo: { $in: [issue._id, ...(issue.childInfo?.map((it) => it.childId) ?? [])] } }}
|
||||
config={[
|
||||
'$lookup.attachedTo',
|
||||
'',
|
||||
|
@ -27,7 +27,7 @@
|
||||
export let value: Employee | null
|
||||
export let _class: Ref<Class<Project | Sprint>>
|
||||
export let size: IconSize = 'x-small'
|
||||
export let parentId: Ref<Project>
|
||||
export let parentId: Ref<Doc>
|
||||
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
@ -54,7 +54,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
const currentParent = await client.findOne(_class, { _id: parentId })
|
||||
const currentParent = await client.findOne(_class, { _id: parentId as Ref<Project> })
|
||||
|
||||
if (currentParent === undefined) {
|
||||
return
|
||||
|
@ -13,62 +13,84 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { DocumentQuery, FindOptions, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { DocumentQuery, WithLookup } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Sprint } from '@hcengineering/tracker'
|
||||
import { Button, defaultSP, Icon, IconAdd, Label, Scroller, showPopup } from '@hcengineering/ui'
|
||||
import { Button, IconAdd, Label, SearchEdit, showPopup } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import {
|
||||
FilterBar,
|
||||
FilterButton,
|
||||
getActiveViewletId,
|
||||
getViewOptions,
|
||||
setActiveViewletId,
|
||||
ViewletSettingButton
|
||||
} from '@hcengineering/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import { getIncludedSprintStatuses, sprintTitleMap, SprintViewMode } from '../../utils'
|
||||
import NewSprint from './NewSprint.svelte'
|
||||
import SprintDatePresenter from './SprintDatePresenter.svelte'
|
||||
import SprintListBrowser from './SprintListBrowser.svelte'
|
||||
import SprintProjectEditor from './SprintProjectEditor.svelte'
|
||||
import SprintContent from './SprintContent.svelte'
|
||||
|
||||
export let label: IntlString
|
||||
export let query: DocumentQuery<Sprint> = {}
|
||||
export let search: string = ''
|
||||
export let mode: SprintViewMode = 'all'
|
||||
|
||||
const ENTRIES_LIMIT = 200
|
||||
const resultSprintsQuery = createQuery()
|
||||
|
||||
const sprintOptions: FindOptions<Sprint> = {
|
||||
sort: { startDate: SortingOrder.Descending },
|
||||
limit: ENTRIES_LIMIT,
|
||||
lookup: {
|
||||
lead: contact.class.Employee,
|
||||
project: tracker.class.Project
|
||||
}
|
||||
const space = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
|
||||
const showCreateDialog = async () => {
|
||||
showPopup(NewSprint, { space, targetElement: null }, 'top')
|
||||
}
|
||||
|
||||
let resultSprints: WithLookup<Sprint>[] = []
|
||||
export let panelWidth: number = 0
|
||||
|
||||
let viewlet: WithLookup<Viewlet> | undefined = undefined
|
||||
|
||||
let searchQuery: DocumentQuery<Sprint> = { ...query }
|
||||
function updateSearchQuery (search: string): void {
|
||||
searchQuery = search === '' ? { ...query } : { ...query, $search: search }
|
||||
}
|
||||
$: if (query) updateSearchQuery(search)
|
||||
|
||||
$: includedSprintStatuses = getIncludedSprintStatuses(mode)
|
||||
$: title = sprintTitleMap[mode]
|
||||
$: includedSprintsQuery = { status: { $in: includedSprintStatuses } }
|
||||
|
||||
$: baseQuery = {
|
||||
...includedSprintsQuery,
|
||||
...query
|
||||
const client = getClient()
|
||||
let resultQuery: DocumentQuery<Sprint> = { ...searchQuery }
|
||||
|
||||
let viewlets: WithLookup<Viewlet>[] = []
|
||||
|
||||
$: update()
|
||||
|
||||
async function update (): Promise<void> {
|
||||
viewlets = await client.findAll(
|
||||
view.class.Viewlet,
|
||||
{ attachTo: tracker.class.Sprint },
|
||||
{
|
||||
lookup: {
|
||||
descriptor: view.class.ViewletDescriptor
|
||||
}
|
||||
}
|
||||
)
|
||||
const _id = getActiveViewletId()
|
||||
viewlet = viewlets.find((viewlet) => viewlet._id === _id) || viewlets[0]
|
||||
setActiveViewletId(viewlet._id)
|
||||
}
|
||||
|
||||
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
|
||||
|
||||
$: resultSprintsQuery.query<Sprint>(
|
||||
tracker.class.Sprint,
|
||||
{ ...resultQuery },
|
||||
(result) => {
|
||||
resultSprints = result
|
||||
},
|
||||
sprintOptions
|
||||
)
|
||||
|
||||
const space = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
|
||||
const showCreateDialog = async () => {
|
||||
showPopup(NewSprint, { space, targetElement: null }, 'top')
|
||||
let asideFloat: boolean = false
|
||||
let asideShown: boolean = true
|
||||
$: if (panelWidth < 900 && !asideFloat) asideFloat = true
|
||||
$: if (panelWidth >= 900 && asideFloat) {
|
||||
asideFloat = false
|
||||
asideShown = false
|
||||
}
|
||||
let docWidth: number
|
||||
let docSize: boolean = false
|
||||
$: if (docWidth <= 900 && !docSize) docSize = true
|
||||
$: if (docWidth > 900 && docSize) docSize = false
|
||||
|
||||
$: viewOptions = getViewOptions(viewlet)
|
||||
|
||||
const handleViewModeChanged = (newMode: SprintViewMode) => {
|
||||
if (newMode === undefined || newMode === mode) {
|
||||
@ -77,18 +99,25 @@
|
||||
|
||||
mode = newMode
|
||||
}
|
||||
|
||||
const retrieveMembers = (s: Sprint) => s.members
|
||||
</script>
|
||||
|
||||
<div class="fs-title flex-between header">
|
||||
<div class="flex-center">
|
||||
<div class="flex-row-center">
|
||||
<Label {label} />
|
||||
<div class="projectTitle">
|
||||
› <Label label={title} />
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<FilterButton _class={tracker.class.Issue} {space} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row-center gap-2">
|
||||
<SearchEdit bind:value={search} on:change={() => {}} />
|
||||
<Button size="small" icon={IconAdd} label={tracker.string.Sprint} kind={'primary'} on:click={showCreateDialog} />
|
||||
{#if viewlet}
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||
{/if}
|
||||
</div>
|
||||
<Button size="small" icon={IconAdd} label={tracker.string.Sprint} kind={'primary'} on:click={showCreateDialog} />
|
||||
</div>
|
||||
<div class="itemsContainer">
|
||||
<div class="flex-center">
|
||||
@ -130,66 +159,23 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="ml-3 filterButton">
|
||||
<Button
|
||||
size="small"
|
||||
icon={IconAdd}
|
||||
kind={'link-bordered'}
|
||||
borderStyle={'dashed'}
|
||||
label={tracker.string.Filter}
|
||||
on:click={() => {}}
|
||||
/>
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- <div class="flex-center">
|
||||
<div class="flex-center">
|
||||
<div class="buttonWrapper">
|
||||
<Button selected size="small" shape="rectangle-right" icon={tracker.icon.ProjectsList} />
|
||||
</div>
|
||||
<div class="buttonWrapper">
|
||||
<Button size="small" shape="rectangle-left" icon={tracker.icon.ProjectsTimeline} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<Button size="small" icon={IconOptions} />
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="w-full h-full clear-mins">
|
||||
<Scroller fade={defaultSP}>
|
||||
<SprintListBrowser
|
||||
_class={tracker.class.Sprint}
|
||||
itemsConfig={[
|
||||
{ key: '', presenter: Icon, props: { icon: tracker.icon.Sprint, size: 'small' } },
|
||||
{ key: '', presenter: tracker.component.SprintPresenter, props: { kind: 'list' } },
|
||||
{ key: '', presenter: SprintProjectEditor, props: { kind: 'list' } },
|
||||
{
|
||||
key: '$lookup.lead',
|
||||
presenter: tracker.component.LeadPresenter,
|
||||
props: {
|
||||
_class: tracker.class.Sprint,
|
||||
defaultClass: contact.class.Employee,
|
||||
shouldShowLabel: false,
|
||||
size: 'x-small'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
presenter: contact.component.MembersPresenter,
|
||||
props: {
|
||||
kind: 'link',
|
||||
intlTitle: tracker.string.SprintMembersTitle,
|
||||
intlSearchPh: tracker.string.SprintMembersSearchPlaceholder,
|
||||
retrieveMembers
|
||||
}
|
||||
},
|
||||
{ key: '', presenter: SprintDatePresenter, props: { field: 'startDate' } },
|
||||
{ key: '', presenter: SprintDatePresenter, props: { field: 'targetDate' } },
|
||||
{ key: '', presenter: tracker.component.SprintStatusPresenter }
|
||||
]}
|
||||
sprints={resultSprints}
|
||||
/>
|
||||
</Scroller>
|
||||
<FilterBar
|
||||
_class={tracker.class.Sprint}
|
||||
query={searchQuery}
|
||||
{viewOptions}
|
||||
on:change={(e) => (resultQuery = e.detail)}
|
||||
/>
|
||||
<div class="flex w-full h-full clear-mins">
|
||||
{#if viewlet}
|
||||
<SprintContent {viewlet} query={{ ...resultQuery, ...includedSprintsQuery }} {space} {viewOptions} />
|
||||
{/if}
|
||||
{#if $$slots.aside !== undefined && asideShown}
|
||||
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@ -220,8 +206,4 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// .filterButton {
|
||||
// color: var(--caption-color);
|
||||
// }
|
||||
</style>
|
||||
|
@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { Sprint } from '@hcengineering/tracker'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import tracker from '../../plugin'
|
||||
import NewSprint from './NewSprint.svelte'
|
||||
|
||||
export let viewlet: WithLookup<Viewlet>
|
||||
export let query: DocumentQuery<Sprint> = {}
|
||||
export let space: Ref<Space> | undefined
|
||||
|
||||
// Extra properties
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
const createItemDialog = NewSprint
|
||||
const createItemLabel = tracker.string.CreateSprint
|
||||
|
||||
const retrieveMembers = (s: Sprint) => 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}
|
||||
<Component
|
||||
is={viewlet.$lookup.descriptor.component}
|
||||
props={{
|
||||
_class: tracker.class.Sprint,
|
||||
config: updateConfig(viewlet.config),
|
||||
options: viewlet.options,
|
||||
createItemDialog,
|
||||
createItemLabel,
|
||||
viewlet,
|
||||
viewOptions,
|
||||
viewOptionsConfig: viewlet.viewOptions?.other,
|
||||
space,
|
||||
query,
|
||||
props: {}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
@ -0,0 +1,105 @@
|
||||
<!--
|
||||
// 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, UsersPopup } from '@hcengineering/presentation'
|
||||
import { Sprint } 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 '../projects/LeadPopup.svelte'
|
||||
|
||||
export let value: Employee | null
|
||||
export let size: IconSize = 'x-small'
|
||||
export let object: Sprint
|
||||
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.ProjectLeadSearchPlaceholder
|
||||
},
|
||||
eventToHTMLElement(event),
|
||||
handleLeadChanged
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value && presenter}
|
||||
<svelte:component
|
||||
this={presenter.presenter}
|
||||
{value}
|
||||
{defaultName}
|
||||
avatarSize={size}
|
||||
isInteractive={true}
|
||||
shouldShowPlaceholder={true}
|
||||
shouldShowName={shouldShowLabel}
|
||||
onEmployeeEdit={handleLeadEditorOpened}
|
||||
tooltipLabels={{ component: LeadPopup, props: { lead: value } }}
|
||||
/>
|
||||
{:else if presenter}
|
||||
<svelte:component
|
||||
this={presenter.presenter}
|
||||
{value}
|
||||
{defaultName}
|
||||
avatarSize={size}
|
||||
isInteractive={true}
|
||||
shouldShowPlaceholder={true}
|
||||
shouldShowName={shouldShowLabel}
|
||||
onEmployeeEdit={handleLeadEditorOpened}
|
||||
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
|
||||
/>
|
||||
{/if}
|
@ -1,292 +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 from '@hcengineering/contact'
|
||||
import { Class, Doc, FindOptions, getObjectValue, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue, Project, Sprint } from '@hcengineering/tracker'
|
||||
import { CheckBox, ExpandCollapse, Spinner, tooltip } from '@hcengineering/ui'
|
||||
import { BuildModelKey } from '@hcengineering/view'
|
||||
import { buildModel, LoadingProps } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import SprintProjectEditor from './SprintProjectEditor.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let itemsConfig: (BuildModelKey | string)[]
|
||||
export let selectedObjectIds: Doc[] = []
|
||||
export let selectedRowIndex: number | undefined = undefined
|
||||
export let sprints: WithLookup<Sprint>[] | undefined = undefined
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
const objectRefs: HTMLElement[] = []
|
||||
|
||||
const baseOptions: FindOptions<Issue> = {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus
|
||||
}
|
||||
}
|
||||
|
||||
$: options = { ...baseOptions } as FindOptions<Sprint>
|
||||
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
||||
$: objectRefs.length = sprints?.length ?? 0
|
||||
|
||||
$: byProject = sprints?.reduce((s, cur) => {
|
||||
const pid = cur.project ?? ''
|
||||
s.set(pid, [...(s.get(pid) ?? []), cur])
|
||||
return s
|
||||
}, new Map<Ref<Project> | '', WithLookup<Sprint>[]>())
|
||||
|
||||
export const onObjectChecked = (docs: Doc[], value: boolean) => {
|
||||
dispatch('check', { docs, value })
|
||||
}
|
||||
|
||||
const handleRowFocused = (object: Doc) => {
|
||||
dispatch('row-focus', object)
|
||||
}
|
||||
|
||||
export const onElementSelected = (offset: 1 | -1 | 0, docObject?: Doc) => {
|
||||
if (!sprints) {
|
||||
return
|
||||
}
|
||||
|
||||
let position =
|
||||
(docObject !== undefined ? sprints?.findIndex((x) => x._id === docObject?._id) : selectedRowIndex) ?? -1
|
||||
|
||||
position += offset
|
||||
|
||||
if (position < 0) {
|
||||
position = 0
|
||||
}
|
||||
|
||||
if (position >= sprints.length) {
|
||||
position = sprints.length - 1
|
||||
}
|
||||
|
||||
const objectRef = objectRefs[position]
|
||||
|
||||
selectedRowIndex = position
|
||||
|
||||
handleRowFocused(sprints[position])
|
||||
|
||||
if (objectRef) {
|
||||
objectRef.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
}
|
||||
}
|
||||
|
||||
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
|
||||
if (options?.limit && options?.limit > 0) {
|
||||
return Math.min(options.limit, props.length)
|
||||
}
|
||||
|
||||
return props.length
|
||||
}
|
||||
|
||||
const isCollapsedMap: Record<any, boolean> = {}
|
||||
|
||||
$: {
|
||||
const exkeys = new Set(Object.keys(isCollapsedMap))
|
||||
for (const c of byProject?.keys() ?? []) {
|
||||
if (!exkeys.delete(c)) {
|
||||
isCollapsedMap[c] = false
|
||||
}
|
||||
}
|
||||
for (const k of exkeys) {
|
||||
delete isCollapsedMap[k]
|
||||
}
|
||||
}
|
||||
|
||||
const handleCollapseCategory = (category: any) => (isCollapsedMap[category] = !isCollapsedMap[category])
|
||||
</script>
|
||||
|
||||
{#await buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }) then itemModels}
|
||||
<div class="listRoot">
|
||||
{#if sprints}
|
||||
{#each Array.from(byProject?.entries() ?? []) as e}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(e[0])}>
|
||||
<div class="flex-row-center gap-2 clear-mins">
|
||||
<SprintProjectEditor
|
||||
isEditable={false}
|
||||
value={e[1][0]}
|
||||
enlargedText={true}
|
||||
kind={'list-header'}
|
||||
shouldShowPlaceholder={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ExpandCollapse isExpanded={!isCollapsedMap[e[0]]} duration={400}>
|
||||
{#each e[1] as docObject (docObject._id)}
|
||||
<div
|
||||
bind:this={objectRefs[sprints.findIndex((x) => x === docObject)]}
|
||||
class="listGrid"
|
||||
class:mListGridChecked={selectedObjectIdsSet.has(docObject._id)}
|
||||
class:mListGridFixed={selectedRowIndex === sprints.findIndex((x) => x === docObject)}
|
||||
class:mListGridSelected={selectedRowIndex === sprints.findIndex((x) => x === docObject)}
|
||||
on:focus={() => {}}
|
||||
on:mouseover={() => handleRowFocused(docObject)}
|
||||
>
|
||||
<div class="contentWrapper">
|
||||
{#each itemModels as attributeModel, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<div
|
||||
class="eListGridCheckBox"
|
||||
use:tooltip={{ direction: 'bottom', label: tracker.string.SelectIssue }}
|
||||
>
|
||||
<CheckBox
|
||||
checked={selectedObjectIdsSet.has(docObject._id)}
|
||||
on:value={(event) => {
|
||||
onObjectChecked([docObject], event.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="iconPresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if attributeModelIndex === 1}
|
||||
<div class="projectPresenter flex-grow">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
<div class="filler" />
|
||||
{:else}
|
||||
<div class="gridElement">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
parentId={docObject._id}
|
||||
sprintId={docObject._id}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</ExpandCollapse>
|
||||
{/each}
|
||||
{:else if loadingProps !== undefined}
|
||||
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
|
||||
<div class="listGrid" class:fixed={rowIndex === selectedRowIndex}>
|
||||
<div class="contentWrapper">
|
||||
<div class="gridElement">
|
||||
<CheckBox checked={false} />
|
||||
<div class="ml-4">
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.listRoot {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.categoryHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0 1.5rem 0 2.25rem;
|
||||
height: 3rem;
|
||||
min-height: 3rem;
|
||||
min-width: 0;
|
||||
background-color: var(--accent-bg-color);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 1.15rem;
|
||||
}
|
||||
|
||||
.listGrid {
|
||||
width: 100%;
|
||||
height: 3.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
|
||||
&.mListGridChecked {
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
|
||||
.eListGridCheckBox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.mListGridSelected {
|
||||
background-color: var(--menu-bg-select);
|
||||
}
|
||||
|
||||
.eListGridCheckBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filler {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gridElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.iconPresenter {
|
||||
padding-left: 0.45rem;
|
||||
}
|
||||
|
||||
.projectPresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 5.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -1,72 +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 type { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { Sprint } from '@hcengineering/tracker'
|
||||
import { BuildModelKey } from '@hcengineering/view'
|
||||
import {
|
||||
ActionContext,
|
||||
focusStore,
|
||||
ListSelectionProvider,
|
||||
LoadingProps,
|
||||
SelectDirection,
|
||||
selectionStore
|
||||
} from '@hcengineering/view-resources'
|
||||
import { onMount } from 'svelte'
|
||||
import SprintList from './SprintList.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let itemsConfig: (BuildModelKey | string)[]
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
export let sprints: WithLookup<Sprint>[] = []
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||
if (dir === 'vertical') {
|
||||
sprintsList.onElementSelected(offset, of)
|
||||
}
|
||||
})
|
||||
|
||||
let sprintsList: SprintList
|
||||
|
||||
$: if (sprintsList !== undefined) {
|
||||
listProvider.update(sprints)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
})
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
context={{
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
|
||||
<SprintList
|
||||
bind:this={sprintsList}
|
||||
{_class}
|
||||
{itemsConfig}
|
||||
{loadingProps}
|
||||
{sprints}
|
||||
selectedObjectIds={$selectionStore ?? []}
|
||||
selectedRowIndex={listProvider.current($focusStore)}
|
||||
on:row-focus={(event) => {
|
||||
listProvider.updateFocus(event.detail ?? undefined)
|
||||
}}
|
||||
on:check={(event) => {
|
||||
listProvider.updateSelection(event.detail.docs, event.detail.value)
|
||||
}}
|
||||
/>
|
@ -70,6 +70,10 @@ import Views from './components/views/Views.svelte'
|
||||
import Statuses from './components/workflow/Statuses.svelte'
|
||||
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
|
||||
import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.svelte'
|
||||
import SprintProjectEditor from './components/sprints/SprintProjectEditor.svelte'
|
||||
import SprintDatePresenter from './components/sprints/SprintDatePresenter.svelte'
|
||||
import SprintLeadPresenter from './components/sprints/SprintLeadPresenter.svelte'
|
||||
|
||||
import {
|
||||
getIssueId,
|
||||
getIssueTitle,
|
||||
@ -380,7 +384,10 @@ export default async (): Promise<Resources> => ({
|
||||
RelatedIssuesSection,
|
||||
RelatedIssueSelector,
|
||||
DeleteProjectPresenter,
|
||||
TimeSpendReportPopup
|
||||
TimeSpendReportPopup,
|
||||
SprintProjectEditor,
|
||||
SprintDatePresenter,
|
||||
SprintLeadPresenter
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||
|
@ -350,6 +350,9 @@ export default mergeIds(trackerId, tracker, {
|
||||
SprintPresenter: '' as AnyComponent,
|
||||
SprintStatusPresenter: '' as AnyComponent,
|
||||
SprintTitlePresenter: '' as AnyComponent,
|
||||
SprintProjectEditor: '' as AnyComponent,
|
||||
SprintDatePresenter: '' as AnyComponent,
|
||||
SprintLeadPresenter: '' as AnyComponent,
|
||||
ReportedTimeEditor: '' as AnyComponent,
|
||||
TimeSpendReport: '' as AnyComponent,
|
||||
EstimationEditor: '' as AnyComponent,
|
||||
|
@ -98,6 +98,7 @@ export { default as ObjectPresenter } from './components/ObjectPresenter.svelte'
|
||||
export { default as TableBrowser } from './components/TableBrowser.svelte'
|
||||
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
||||
export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte'
|
||||
export { default as MarkupPresenter } from './components/MarkupPresenter.svelte'
|
||||
export * from './context'
|
||||
export * from './filter'
|
||||
export * from './selection'
|
||||
|
Loading…
Reference in New Issue
Block a user