mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-16 13:21:57 +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']
|
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(
|
builder.createDoc(
|
||||||
presentation.class.ObjectSearchCategory,
|
presentation.class.ObjectSearchCategory,
|
||||||
core.space.Model,
|
core.space.Model,
|
||||||
@ -1600,4 +1604,78 @@ export function createModel (builder: Builder): void {
|
|||||||
view.component.NumberPresenter,
|
view.component.NumberPresenter,
|
||||||
tracker.component.ReportedTimeEditor
|
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: {
|
action: {
|
||||||
NewRelatedIssue: '' as Ref<Action<Doc, Record<string, any>>>,
|
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}
|
{#each requests as request}
|
||||||
{#await getType(request) then type}
|
{#await getType(request) then type}
|
||||||
{#if type}
|
{#if type}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="request flex-center"
|
class="request flex-center"
|
||||||
class:cursor-pointer={editable}
|
class:cursor-pointer={editable}
|
||||||
|
@ -148,7 +148,7 @@
|
|||||||
|
|
||||||
const reportQuery = createQuery()
|
const reportQuery = createQuery()
|
||||||
|
|
||||||
import tracker from '@hcengineering/tracker'
|
import tracker, { Issue } from '@hcengineering/tracker'
|
||||||
import { EmployeeReports, fromTzDate, getEndDate, getStartDate } from '../utils'
|
import { EmployeeReports, fromTzDate, getEndDate, getStartDate } from '../utils'
|
||||||
|
|
||||||
let timeReports: Map<Ref<Employee>, EmployeeReports> = new Map()
|
let timeReports: Map<Ref<Employee>, EmployeeReports> = new Map()
|
||||||
@ -163,19 +163,26 @@
|
|||||||
const newMap = new Map<Ref<Employee>, EmployeeReports>()
|
const newMap = new Map<Ref<Employee>, EmployeeReports>()
|
||||||
for (const r of res) {
|
for (const r of res) {
|
||||||
if (r.employee != null) {
|
if (r.employee != null) {
|
||||||
const or = newMap.get(r.employee)
|
const or = newMap.get(r.employee) ?? {
|
||||||
newMap.set(r.employee, { value: (or?.value ?? 0) + r.value, reports: [...(or?.reports ?? []), r] })
|
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
|
timeReports = newMap
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
lookup: {
|
lookup: {
|
||||||
_id: {
|
|
||||||
attachedTo: tracker.class.Issue
|
attachedTo: tracker.class.Issue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -83,6 +83,18 @@
|
|||||||
function getOverrideConfig (startDate: Date): Map<string, BuildModelKey> {
|
function getOverrideConfig (startDate: Date): Map<string, BuildModelKey> {
|
||||||
const typevals = getTypeVals(startDate)
|
const typevals = getTypeVals(startDate)
|
||||||
const endDate = getEndDate(startDate.getFullYear(), startDate.getMonth())
|
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>([
|
return new Map<string, BuildModelKey>([
|
||||||
[
|
[
|
||||||
'@wdCount',
|
'@wdCount',
|
||||||
@ -109,12 +121,38 @@
|
|||||||
presenter: ReportPresenter,
|
presenter: ReportPresenter,
|
||||||
props: {
|
props: {
|
||||||
month: startDate ?? getStartDate(currentDate.getFullYear(), currentDate.getMonth()),
|
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',
|
sortingKey: '@wdCountReported',
|
||||||
sortingFunction: (a: Doc, b: Doc) =>
|
sortingFunction: (a: Doc, b: Doc) => getReport(b._id).value - getReport(a._id).value
|
||||||
getTotal(getStatRequests(b._id as Ref<Staff>, startDate), startDate, endDate, types) -
|
}
|
||||||
getTotal(getStatRequests(a._id as Ref<Staff>, startDate), startDate, endDate, types)
|
],
|
||||||
|
[
|
||||||
|
'@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 CreateRequest from '../CreateRequest.svelte'
|
||||||
import RequestsPopup from '../RequestsPopup.svelte'
|
import RequestsPopup from '../RequestsPopup.svelte'
|
||||||
import ScheduleRequests from '../ScheduleRequests.svelte'
|
import ScheduleRequests from '../ScheduleRequests.svelte'
|
||||||
|
import ReportsPopup from './ReportsPopup.svelte'
|
||||||
|
|
||||||
export let currentDate: Date = new Date()
|
export let currentDate: Date = new Date()
|
||||||
|
|
||||||
@ -106,6 +107,13 @@
|
|||||||
bottom: 3.5
|
bottom: 3.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showReportInfo (employee: Staff, rTime: EmployeeReports | undefined): void {
|
||||||
|
if (rTime === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showPopup(ReportsPopup, { employee, reports: rTime.reports }, 'top')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if departmentStaff.length}
|
{#if departmentStaff.length}
|
||||||
@ -152,9 +160,14 @@
|
|||||||
>
|
>
|
||||||
{getTotal(requests, startDate, endDate, types)}
|
{getTotal(requests, startDate, endDate, types)}
|
||||||
</td>
|
</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}
|
{#if rTime !== undefined}
|
||||||
{floorFractionDigits(rTime.value, 3)}
|
{floorFractionDigits(rTime.value, 3)}
|
||||||
|
({rTime.tasks.size})
|
||||||
{:else}
|
{:else}
|
||||||
0
|
0
|
||||||
{/if}
|
{/if}
|
||||||
@ -166,6 +179,7 @@
|
|||||||
{@const tooltipValue = getTooltip(requests)}
|
{@const tooltipValue = getTooltip(requests)}
|
||||||
{@const ww = findReports(employee, day, timeReports)}
|
{@const ww = findReports(employee, day, timeReports)}
|
||||||
{#key [tooltipValue, editable]}
|
{#key [tooltipValue, editable]}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<td
|
<td
|
||||||
class="w-9 max-w-9 min-w-9"
|
class="w-9 max-w-9 min-w-9"
|
||||||
class:today={areDatesEqual(todayDate, day)}
|
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 { Ref, TxOperations } from '@hcengineering/core'
|
||||||
import { Department, Request, RequestType, Staff, TzDate } from '@hcengineering/hr'
|
import { Department, Request, RequestType, Staff, TzDate } from '@hcengineering/hr'
|
||||||
import { MessageBox } from '@hcengineering/presentation'
|
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 { isWeekend, MILLISECONDS_IN_DAY, showPopup } from '@hcengineering/ui'
|
||||||
import hr from './plugin'
|
import hr from './plugin'
|
||||||
|
|
||||||
@ -235,5 +235,6 @@ export function tableToCSV (tableId: string, separator = ','): string {
|
|||||||
|
|
||||||
export interface EmployeeReports {
|
export interface EmployeeReports {
|
||||||
reports: TimeSpendReport[]
|
reports: TimeSpendReport[]
|
||||||
|
tasks: Map<Ref<Issue>, Issue>
|
||||||
value: number
|
value: number
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
<TableBrowser
|
<TableBrowser
|
||||||
showFilterBar={false}
|
showFilterBar={false}
|
||||||
_class={tracker.class.TimeSpendReport}
|
_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={[
|
config={[
|
||||||
'$lookup.attachedTo',
|
'$lookup.attachedTo',
|
||||||
'',
|
'',
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
export let value: Employee | null
|
export let value: Employee | null
|
||||||
export let _class: Ref<Class<Project | Sprint>>
|
export let _class: Ref<Class<Project | Sprint>>
|
||||||
export let size: IconSize = 'x-small'
|
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 defaultClass: Ref<Class<Doc>> | undefined = undefined
|
||||||
export let isEditable: boolean = true
|
export let isEditable: boolean = true
|
||||||
export let shouldShowLabel: boolean = false
|
export let shouldShowLabel: boolean = false
|
||||||
@ -54,7 +54,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentParent = await client.findOne(_class, { _id: parentId })
|
const currentParent = await client.findOne(_class, { _id: parentId as Ref<Project> })
|
||||||
|
|
||||||
if (currentParent === undefined) {
|
if (currentParent === undefined) {
|
||||||
return
|
return
|
||||||
|
@ -13,63 +13,85 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import contact from '@hcengineering/contact'
|
import { DocumentQuery, WithLookup } from '@hcengineering/core'
|
||||||
import { DocumentQuery, FindOptions, SortingOrder, WithLookup } from '@hcengineering/core'
|
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
import { createQuery } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { Sprint } from '@hcengineering/tracker'
|
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 tracker from '../../plugin'
|
||||||
import { getIncludedSprintStatuses, sprintTitleMap, SprintViewMode } from '../../utils'
|
import { getIncludedSprintStatuses, sprintTitleMap, SprintViewMode } from '../../utils'
|
||||||
import NewSprint from './NewSprint.svelte'
|
import NewSprint from './NewSprint.svelte'
|
||||||
import SprintDatePresenter from './SprintDatePresenter.svelte'
|
import SprintContent from './SprintContent.svelte'
|
||||||
import SprintListBrowser from './SprintListBrowser.svelte'
|
|
||||||
import SprintProjectEditor from './SprintProjectEditor.svelte'
|
|
||||||
|
|
||||||
export let label: IntlString
|
export let label: IntlString
|
||||||
export let query: DocumentQuery<Sprint> = {}
|
export let query: DocumentQuery<Sprint> = {}
|
||||||
export let search: string = ''
|
export let search: string = ''
|
||||||
export let mode: SprintViewMode = 'all'
|
export let mode: SprintViewMode = 'all'
|
||||||
|
|
||||||
const ENTRIES_LIMIT = 200
|
const space = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
|
||||||
const resultSprintsQuery = createQuery()
|
const showCreateDialog = async () => {
|
||||||
|
showPopup(NewSprint, { space, targetElement: null }, 'top')
|
||||||
const sprintOptions: FindOptions<Sprint> = {
|
|
||||||
sort: { startDate: SortingOrder.Descending },
|
|
||||||
limit: ENTRIES_LIMIT,
|
|
||||||
lookup: {
|
|
||||||
lead: contact.class.Employee,
|
|
||||||
project: tracker.class.Project
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
$: includedSprintStatuses = getIncludedSprintStatuses(mode)
|
||||||
$: title = sprintTitleMap[mode]
|
$: title = sprintTitleMap[mode]
|
||||||
$: includedSprintsQuery = { status: { $in: includedSprintStatuses } }
|
$: includedSprintsQuery = { status: { $in: includedSprintStatuses } }
|
||||||
|
|
||||||
$: baseQuery = {
|
const client = getClient()
|
||||||
...includedSprintsQuery,
|
let resultQuery: DocumentQuery<Sprint> = { ...searchQuery }
|
||||||
...query
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
|
|
||||||
|
|
||||||
$: resultSprintsQuery.query<Sprint>(
|
|
||||||
tracker.class.Sprint,
|
|
||||||
{ ...resultQuery },
|
|
||||||
(result) => {
|
|
||||||
resultSprints = result
|
|
||||||
},
|
|
||||||
sprintOptions
|
|
||||||
)
|
)
|
||||||
|
const _id = getActiveViewletId()
|
||||||
const space = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
|
viewlet = viewlets.find((viewlet) => viewlet._id === _id) || viewlets[0]
|
||||||
const showCreateDialog = async () => {
|
setActiveViewletId(viewlet._id)
|
||||||
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) => {
|
const handleViewModeChanged = (newMode: SprintViewMode) => {
|
||||||
if (newMode === undefined || newMode === mode) {
|
if (newMode === undefined || newMode === mode) {
|
||||||
return
|
return
|
||||||
@ -77,18 +99,25 @@
|
|||||||
|
|
||||||
mode = newMode
|
mode = newMode
|
||||||
}
|
}
|
||||||
|
|
||||||
const retrieveMembers = (s: Sprint) => s.members
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fs-title flex-between header">
|
<div class="fs-title flex-between header">
|
||||||
<div class="flex-center">
|
<div class="flex-row-center">
|
||||||
<Label {label} />
|
<Label {label} />
|
||||||
<div class="projectTitle">
|
<div class="projectTitle">
|
||||||
› <Label label={title} />
|
› <Label label={title} />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<FilterButton _class={tracker.class.Issue} {space} />
|
||||||
</div>
|
</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} />
|
<Button size="small" icon={IconAdd} label={tracker.string.Sprint} kind={'primary'} on:click={showCreateDialog} />
|
||||||
|
{#if viewlet}
|
||||||
|
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="itemsContainer">
|
<div class="itemsContainer">
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
@ -130,66 +159,23 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<div class="ml-3">
|
<FilterBar
|
||||||
<Button size="small" icon={IconOptions} />
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
<div class="w-full h-full clear-mins">
|
|
||||||
<Scroller fade={defaultSP}>
|
|
||||||
<SprintListBrowser
|
|
||||||
_class={tracker.class.Sprint}
|
_class={tracker.class.Sprint}
|
||||||
itemsConfig={[
|
query={searchQuery}
|
||||||
{ key: '', presenter: Icon, props: { icon: tracker.icon.Sprint, size: 'small' } },
|
{viewOptions}
|
||||||
{ key: '', presenter: tracker.component.SprintPresenter, props: { kind: 'list' } },
|
on:change={(e) => (resultQuery = e.detail)}
|
||||||
{ 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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -220,8 +206,4 @@
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// .filterButton {
|
|
||||||
// color: var(--caption-color);
|
|
||||||
// }
|
|
||||||
</style>
|
</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 Statuses from './components/workflow/Statuses.svelte'
|
||||||
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
|
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
|
||||||
import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.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 {
|
import {
|
||||||
getIssueId,
|
getIssueId,
|
||||||
getIssueTitle,
|
getIssueTitle,
|
||||||
@ -380,7 +384,10 @@ export default async (): Promise<Resources> => ({
|
|||||||
RelatedIssuesSection,
|
RelatedIssuesSection,
|
||||||
RelatedIssueSelector,
|
RelatedIssueSelector,
|
||||||
DeleteProjectPresenter,
|
DeleteProjectPresenter,
|
||||||
TimeSpendReportPopup
|
TimeSpendReportPopup,
|
||||||
|
SprintProjectEditor,
|
||||||
|
SprintDatePresenter,
|
||||||
|
SprintLeadPresenter
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||||
|
@ -350,6 +350,9 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
SprintPresenter: '' as AnyComponent,
|
SprintPresenter: '' as AnyComponent,
|
||||||
SprintStatusPresenter: '' as AnyComponent,
|
SprintStatusPresenter: '' as AnyComponent,
|
||||||
SprintTitlePresenter: '' as AnyComponent,
|
SprintTitlePresenter: '' as AnyComponent,
|
||||||
|
SprintProjectEditor: '' as AnyComponent,
|
||||||
|
SprintDatePresenter: '' as AnyComponent,
|
||||||
|
SprintLeadPresenter: '' as AnyComponent,
|
||||||
ReportedTimeEditor: '' as AnyComponent,
|
ReportedTimeEditor: '' as AnyComponent,
|
||||||
TimeSpendReport: '' as AnyComponent,
|
TimeSpendReport: '' as AnyComponent,
|
||||||
EstimationEditor: '' 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 TableBrowser } from './components/TableBrowser.svelte'
|
||||||
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
||||||
export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte'
|
export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte'
|
||||||
|
export { default as MarkupPresenter } from './components/MarkupPresenter.svelte'
|
||||||
export * from './context'
|
export * from './context'
|
||||||
export * from './filter'
|
export * from './filter'
|
||||||
export * from './selection'
|
export * from './selection'
|
||||||
|
Loading…
Reference in New Issue
Block a user