Merge branch 'main' of github.com:hcengineering/anticrm into show-filter-items-current-space

Signed-off-by: Denis Bunakalya <denis.bunakalya@xored.com>
This commit is contained in:
Denis Bunakalya
2023-01-18 11:53:37 +04:00
parent 328a052d8f
commit 49bf38cf0a
221 changed files with 3593 additions and 2272 deletions
@@ -15,17 +15,16 @@
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { Issue, IssueTemplate } from '@hcengineering/tracker'
import { UsersPopup, getClient } from '@hcengineering/presentation'
import { AttributeModel } from '@hcengineering/view'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { getObjectPresenter } from '@hcengineering/view-resources'
import { IntlString } from '@hcengineering/platform'
import { getClient, UsersPopup } from '@hcengineering/presentation'
import { Issue, IssueTemplate } from '@hcengineering/tracker'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources'
import tracker from '../../plugin'
export let value: Employee | null | undefined
export let issueId: Ref<Issue>
export let issueClass: Ref<Class<Issue | IssueTemplate>> = tracker.class.Issue
export let object: Issue | IssueTemplate
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false
@@ -52,15 +51,9 @@
return
}
const currentIssue = await client.findOne(issueClass, { _id: issueId })
if (currentIssue === undefined) {
return
}
const newAssignee = result === null ? null : result._id
await client.update(currentIssue, { assignee: newAssignee })
await client.update(object, { assignee: newAssignee })
}
const handleAssigneeEditorOpened = async (event: MouseEvent) => {
@@ -44,14 +44,6 @@
}
}
$: tooltipValue = new Date(value).toLocaleString('default', {
minute: '2-digit',
hour: 'numeric',
day: '2-digit',
month: 'short',
year: 'numeric'
})
$: formatTime(value)
</script>
@@ -1,12 +0,0 @@
<span class="root" />
<style lang="scss">
.root {
display: flex;
flex-grow: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
flex-shrink: 10;
}
</style>
@@ -1,22 +1,22 @@
<script lang="ts">
import { fade } from 'svelte/transition'
import {
NotificationSeverity,
Notification,
Button,
Icon,
IconClose,
IconInfo,
IconCheckCircle,
Label,
showPanel
} from '@hcengineering/ui'
import { copyTextToClipboard, createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus } from '@hcengineering/tracker'
import {
AnySvelteComponent,
Button,
Icon,
IconCheckCircle,
IconClose,
IconInfo,
Notification,
NotificationSeverity,
showPanel
} from '@hcengineering/ui'
import { fade } from 'svelte/transition'
import IssueStatusIcon from './IssueStatusIcon.svelte'
import IssuePresenter from './IssuePresenter.svelte'
import tracker from '../../plugin'
import IssuePresenter from './IssuePresenter.svelte'
import IssueStatusIcon from './IssueStatusIcon.svelte'
export let notification: Notification
export let onRemove: () => void
@@ -31,7 +31,7 @@
$: issueQuery.query(
tracker.class.Issue,
{ _id: params.issueId },
{ _id: params?.issueId },
(res) => {
issue = res[0]
},
@@ -49,7 +49,7 @@
)
}
const getIcon = () => {
const getIcon = (): AnySvelteComponent | undefined => {
switch (severity) {
case NotificationSeverity.Success:
return IconCheckCircle
@@ -84,14 +84,17 @@
copyTextToClipboard(params?.issueUrl)
}
}
$: icon = getIcon()
</script>
<div class="root" in:fade out:fade>
<Icon icon={getIcon()} size="medium" fill={getIconColor()} />
{#if icon}
<Icon {icon} size="medium" fill={getIconColor()} />
{/if}
<div class="content">
<div class="title">
<Label label={title} />
{title}
</div>
<div class="row">
<div class="issue">
@@ -105,7 +108,7 @@
{subTitle}
</div>
<div class="postfix">
{params.subTitlePostfix}
{params?.subTitlePostfix}
</div>
</div>
</div>
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { WithLookup } from '@hcengineering/core'
import { Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import type { Issue, Team } from '@hcengineering/tracker'
import { showPanel } from '@hcengineering/ui'
@@ -23,6 +23,9 @@
export let disableClick = false
export let onClick: (() => void) | undefined = undefined
// Extra properties
export let teams: Map<Ref<Team>, Team> | undefined = undefined
function handleIssueEditorOpened () {
if (disableClick) {
return
@@ -38,16 +41,21 @@
const spaceQuery = createQuery()
let currentTeam: Team | undefined = value?.$lookup?.space
$: if (value && value?.$lookup?.space === undefined) {
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
$: if (teams === undefined) {
if (value && value?.$lookup?.space === undefined) {
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
} else {
spaceQuery.unsubscribe()
}
} else {
spaceQuery.unsubscribe()
currentTeam = teams.get(value.space)
}
$: title = currentTeam ? `${currentTeam.identifier}-${value?.number}` : `${value?.number}`
</script>
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="issuePresenterRoot"
class:noPointer={disableClick}
@@ -48,7 +48,7 @@
async function updateStatus (
txes: Tx[],
statuses: Map<Ref<IssueStatus>, WithLookup<IssueStatus>>,
now: number
_: number
): Promise<void> {
const result: WithTime[] = []
@@ -24,6 +24,8 @@
export let size: IconSize
export let fill: string | undefined = undefined
export let issueStatuses: IssueStatus[] | undefined = undefined
const dynamicFillCategories = [tracker.issueStatusCategory.Started]
const client = getClient()
@@ -36,12 +38,19 @@
} = { index: undefined, count: undefined }
const categoriesQuery = createQuery()
categoriesQuery.query(
tracker.class.IssueStatus,
{ category: tracker.issueStatusCategory.Started },
(res) => (statuses = res),
{ sort: { rank: SortingOrder.Ascending } }
)
$: if (issueStatuses === undefined) {
categoriesQuery.query(
tracker.class.IssueStatus,
{ category: tracker.issueStatusCategory.Started },
(res) => (statuses = res),
{ sort: { rank: SortingOrder.Ascending } }
)
} else {
const _s = [...issueStatuses.filter((it) => it.category === tracker.issueStatusCategory.Started)]
_s.sort((a, b) => a.rank.localeCompare(b.rank))
categoriesQuery.unsubscribe()
}
async function updateCategory (status: WithLookup<IssueStatus>, statuses: IssueStatus[]) {
if (status.$lookup?.category) {
@@ -1,23 +1,39 @@
<script lang="ts">
import { DocumentQuery, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Component } from '@hcengineering/ui'
import { Viewlet } from '@hcengineering/view'
import { Issue } from '@hcengineering/tracker'
import { viewOptionsStore } from '@hcengineering/view-resources'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import tracker from '../../plugin'
import CreateIssue from '../CreateIssue.svelte'
export let viewlet: WithLookup<Viewlet>
export let query: DocumentQuery<Issue> = {}
export let space: Ref<Space> | undefined
// Extra properties
export let teams: Map<Ref<Team>, Team> | undefined
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
export let viewOptions: ViewOptions
const createItemDialog = CreateIssue
const createItemLabel = tracker.string.AddIssueTooltip
</script>
{#if viewlet?.$lookup?.descriptor?.component}
<Component
is={viewlet.$lookup.descriptor.component}
props={{
_class: tracker.class.Issue,
config: viewlet.config,
options: viewlet.options,
createItemDialog,
createItemLabel,
viewlet,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
space,
query,
viewOptions: $viewOptionsStore
props: { teams, issueStatuses }
}}
/>
{/if}
@@ -45,7 +45,7 @@
$: groupedByProject = getGroupedIssues('project', issues)
$: groupedBySprint = getGroupedIssues('sprint', issues)
const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
const handleStatusFilterMenuSectionOpened = () => {
const statusGroups: { [key: string]: number } = {}
for (const status of defaultStatuses) {
@@ -66,7 +66,7 @@
)
}
const handlePriorityFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
const handlePriorityFilterMenuSectionOpened = () => {
const priorityGroups: { [key: string]: number } = {}
for (const priority of defaultPriorities) {
@@ -86,7 +86,7 @@
)
}
const handleProjectFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
const handleProjectFilterMenuSectionOpened = () => {
const projectGroups: { [key: string]: number } = {}
for (const [project, value] of Object.entries(groupedByProject)) {
@@ -105,7 +105,7 @@
)
}
const handleSprintFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
const handleSprintFilterMenuSectionOpened = () => {
const sprintGroups: { [key: string]: number } = {}
for (const [project, value] of Object.entries(groupedBySprint)) {
@@ -1,355 +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, FindOptions, Ref, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import ui, {
ActionIcon,
Button,
CheckBox,
Component,
eventToHTMLElement,
ExpandCollapse,
getEventPositionElement,
IconAdd,
IconMoreH,
Label,
showPopup,
Spinner
} from '@hcengineering/ui'
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
import {
buildModel,
filterStore,
FixedColumn,
getObjectPresenter,
LoadingProps,
Menu
} from '@hcengineering/view-resources'
import { onDestroy } from 'svelte'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import { IssuesGroupByKeys, issuesGroupEditorMap, IssuesOrderByKeys, issuesSortOrderMap } from '../../utils'
import CreateIssue from '../CreateIssue.svelte'
import IssueStatistics from '../sprints/IssueStatistics.svelte'
import IssuesListItem from './IssuesListItem.svelte'
export let _class: Ref<Class<Doc>>
export let currentSpace: Ref<Team> | undefined = undefined
export let groupByKey: IssuesGroupByKeys | undefined = undefined
export let orderBy: IssuesOrderByKeys
export let statuses: WithLookup<IssueStatus>[]
export let employees: (WithLookup<Employee> | undefined)[] = []
export let categories: any[] = []
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let itemsConfig: (BuildModelKey | string)[]
export let selectedObjectIds: Doc[] = []
export let selectedRowIndex: number | undefined = undefined
export let groupedIssues: { [key: string | number | symbol]: Issue[] } = {}
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,
space: tracker.class.Team,
_id: {
subIssues: tracker.class.Issue
}
}
}
const categoryLimit: Record<any, number> = {}
const spaceQuery = createQuery()
const defaultLimit = 20
const autoFoldLimit = 20
const singleCategoryLimit = 200
const noCategory = '#no_category'
let currentTeam: Team | undefined
let personPresenter: AttributeModel
let isCollapsedMap: Record<any, boolean> = {}
let itemModels: AttributeModel[]
let isFilterUpdate = false
let groupedIssuesBeforeFilter = groupedIssues
const handleMenuOpened = async (event: MouseEvent, object: Doc, rowIndex: number) => {
event.preventDefault()
selectedRowIndex = rowIndex
if (!selectedObjectIdsSet.has(object._id)) {
onObjectChecked(combinedGroupedIssues, false)
selectedObjectIds = []
}
const items = selectedObjectIds.length > 0 ? selectedObjectIds : object
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(event), () => {
selectedRowIndex = undefined
})
}
export const onObjectChecked = (docs: Doc[], value: boolean) => {
dispatch('check', { docs, value })
}
const handleRowFocused = (object: Doc) => {
dispatch('row-focus', object)
}
const handleNewIssueAdded = (event: MouseEvent, category: any) => {
if (!currentSpace) {
return
}
showPopup(
CreateIssue,
{ space: currentSpace, ...(groupByKey ? { [groupByKey]: category } : {}) },
eventToHTMLElement(event)
)
}
function toCat (category: any): any {
return 'cat-' + (category ?? noCategory)
}
const handleCollapseCategory = (category: any) => {
isCollapsedMap[category] = !isCollapsedMap[category]
}
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
if (options?.limit && options?.limit > 0) {
return Math.min(options.limit, props.length)
}
return props.length
}
function limitGroup (
category: any,
groupes: { [key: string | number | symbol]: Issue[] },
categoryLimit: Record<any, number>
): Issue[] {
const issues = groupes[category] ?? []
const initialLimit = Object.keys(groupes).length === 1 ? singleCategoryLimit : defaultLimit
const limit = categoryLimit[toCat(category)] ?? initialLimit
return issues.slice(0, limit)
}
const getInitCollapseValue = (category: any) =>
categories.length === 1 ? false : (groupedIssues[category]?.length ?? 0) > autoFoldLimit
const unsubscribeFilter = filterStore.subscribe(() => (isFilterUpdate = true))
onDestroy(unsubscribeFilter)
$: {
if (isFilterUpdate && groupedIssuesBeforeFilter !== groupedIssues && groupByKey) {
isCollapsedMap = {}
categories.forEach((category) => (isCollapsedMap[toCat(category)] = getInitCollapseValue(category)))
isFilterUpdate = false
groupedIssuesBeforeFilter = groupedIssues
}
}
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
currentTeam = res.shift()
})
$: {
const exkeys = new Set(Object.keys(isCollapsedMap))
for (const c of categories) {
if (!exkeys.delete(toCat(c))) {
isCollapsedMap[toCat(c)] = getInitCollapseValue(c)
}
}
for (const k of exkeys) {
delete isCollapsedMap[k]
}
}
$: combinedGroupedIssues = Object.values(groupedIssues).flat(1)
$: options = { ...baseOptions, sort: { [orderBy]: issuesSortOrderMap[orderBy] } } as FindOptions<Issue>
$: headerComponent = groupByKey === undefined || groupByKey === 'assignee' ? null : issuesGroupEditorMap[groupByKey]
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
$: objectRefs.length = combinedGroupedIssues.length
$: getObjectPresenter(client, contact.class.Person, { key: '' }).then((p) => {
personPresenter = p
})
$: buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }).then((res) => (itemModels = res))
</script>
<div class="issueslist-container">
{#each categories as category}
{@const items = groupedIssues[category] ?? []}
{@const limited = limitGroup(category, groupedIssues, categoryLimit) ?? []}
{#if headerComponent || groupByKey === 'assignee' || category === undefined}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(toCat(category))}>
<div class="flex-row-center gap-2 clear-mins">
<FixedColumn key={'issuelist_groupBy'} justify={'left'}>
{#if groupByKey === 'assignee' && personPresenter}
<svelte:component
this={personPresenter.presenter}
shouldShowLabel={true}
value={employees.find((x) => x?._id === category)}
defaultName={tracker.string.NoAssignee}
shouldShowPlaceholder={true}
isInteractive={false}
avatarSize={'small'}
enlargedText
{currentSpace}
/>
{:else if !groupByKey}
<span class="text-base fs-bold overflow-label content-accent-color pointer-events-none">
<Label label={tracker.string.NoGrouping} />
</span>
{:else if headerComponent}
<Component
is={headerComponent}
props={{
isEditable: false,
shouldShowLabel: true,
value: groupByKey ? { [groupByKey]: category } : {},
statuses: groupByKey === 'status' ? statuses : undefined,
issues: groupedIssues[category],
width: 'min-content',
kind: 'list-header',
enlargedText: true,
currentSpace
}}
/>
{/if}
</FixedColumn>
<FixedColumn key={'issuelist_statistics'} justify={'left'}>
<IssueStatistics issues={groupedIssues[category]} />
</FixedColumn>
{#if limited.length < items.length}
<div class="counter">
{limited.length}
<div class="text-xs mx-1">/</div>
{items.length}
</div>
<ActionIcon
size={'small'}
icon={IconMoreH}
label={ui.string.ShowMore}
action={() => {
categoryLimit[toCat(category)] = limited.length + 20
}}
/>
{:else}
<span class="counter">{items.length}</span>
{/if}
</div>
<Button
icon={IconAdd}
kind={'transparent'}
showTooltip={{ label: tracker.string.AddIssueTooltip }}
on:click={(event) => handleNewIssueAdded(event, category)}
/>
</div>
{/if}
<ExpandCollapse isExpanded={!isCollapsedMap[toCat(category)]} duration={400}>
{#if itemModels}
{#if groupedIssues[category]}
{#each limited as docObject (docObject._id)}
<IssuesListItem
bind:use={objectRefs[combinedGroupedIssues.findIndex((x) => x === docObject)]}
{docObject}
model={itemModels}
{groupByKey}
selected={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
checked={selectedObjectIdsSet.has(docObject._id)}
{statuses}
{currentTeam}
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
on:contextmenu={(event) =>
handleMenuOpened(
event,
docObject,
combinedGroupedIssues.findIndex((x) => x === docObject)
)}
on:focus={() => {}}
on:mouseover={() => handleRowFocused(docObject)}
/>
{/each}
{:else if loadingProps !== undefined}
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
<div class="listGrid row" class:fixed={rowIndex === selectedRowIndex}>
<div class="flex-center clear-mins h-full">
<div class="gridElement">
<CheckBox checked={false} />
<div class="ml-4">
<Spinner size="small" />
</div>
</div>
</div>
</div>
{/each}
{/if}
{/if}
</ExpandCollapse>
{/each}
</div>
<style lang="scss">
.issueslist-container {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: max-content;
min-width: auto;
min-height: auto;
}
.categoryHeader {
position: sticky;
top: 0;
padding: 0 0.75rem 0 2.25rem;
height: 3rem;
min-height: 3rem;
min-width: 0;
background: var(--header-bg-color);
z-index: 5;
}
.row:not(:last-child) {
border-bottom: 1px solid var(--accent-bg-color);
}
.counter {
display: flex;
align-items: center;
flex-wrap: nowrap;
flex-shrink: 0;
margin-left: 1rem;
padding: 0.25rem 0.5rem;
min-width: 1.325rem;
text-align: center;
font-weight: 500;
font-size: 1rem;
line-height: 1rem;
color: var(--accent-color);
background-color: var(--body-color);
border: 1px solid var(--divider-color);
border-radius: 1rem;
}
</style>
@@ -1,82 +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 { BuildModelKey } from '@hcengineering/view'
import {
ActionContext,
focusStore,
ListSelectionProvider,
SelectDirection,
selectionStore,
LoadingProps
} from '@hcengineering/view-resources'
import IssuesList from './IssuesList.svelte'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Employee } from '@hcengineering/contact'
import { onMount } from 'svelte'
import { IssuesGroupByKeys, IssuesOrderByKeys } from '../../utils'
export let _class: Ref<Class<Doc>>
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let itemsConfig: (BuildModelKey | string)[]
export let currentSpace: Ref<Team> | undefined = undefined
export let groupByKey: IssuesGroupByKeys | undefined = undefined
export let orderBy: IssuesOrderByKeys
export let statuses: WithLookup<IssueStatus>[]
export let employees: (WithLookup<Employee> | undefined)[] = []
export let categories: any[] = []
export let groupedIssues: { [key: string | number | symbol]: Issue[] } = {}
export let loadingProps: LoadingProps | undefined = undefined
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
let issuesList: IssuesList
$: if (issuesList !== undefined) listProvider.update(Object.values(groupedIssues).flat(1))
onMount(() => {
;(document.activeElement as HTMLElement)?.blur()
})
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<IssuesList
bind:this={issuesList}
{_class}
{baseMenuClass}
{currentSpace}
{groupByKey}
{orderBy}
{statuses}
{employees}
{categories}
{itemsConfig}
{groupedIssues}
{loadingProps}
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)
}}
/>
@@ -1,23 +1,20 @@
<script lang="ts">
import { Ref, Space } from '@hcengineering/core'
import { DocumentQuery, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, SortingOrder, Space, WithLookup } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { Button, IconDetails, IconDetailsFilled, location } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Button, IconDetails, IconDetailsFilled } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { FilterBar, ViewOptionModel, ViewOptionsButton, getActiveViewletId } from '@hcengineering/view-resources'
import { FilterBar, getActiveViewletId, getViewOptions } from '@hcengineering/view-resources'
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
import tracker from '../../plugin'
import IssuesContent from './IssuesContent.svelte'
import IssuesHeader from './IssuesHeader.svelte'
import { getDefaultViewOptionsConfig } from '../../utils'
import tracker from '../../plugin'
import { onDestroy } from 'svelte'
export let space: Ref<Space> | undefined = undefined
export let query: DocumentQuery<Issue> = {}
export let title: IntlString | undefined = undefined
export let label: string = ''
export let viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsConfig()
export let panelWidth: number = 0
@@ -39,7 +36,7 @@
async function update (): Promise<void> {
viewlets = await client.findAll(
view.class.Viewlet,
{ attachTo: tracker.class.Issue },
{ attachTo: tracker.class.Issue, variant: { $ne: 'subissue' } },
{
lookup: {
descriptor: view.class.ViewletDescriptor
@@ -67,11 +64,40 @@
$: if (docWidth <= 900 && !docSize) docSize = true
$: if (docWidth > 900 && docSize) docSize = false
onDestroy(
location.subscribe(() => {
viewOptionsConfig = viewOptionsConfig
})
const teamQuery = createQuery()
let _teams: Map<Ref<Team>, Team> | undefined = undefined
let _result: any
$: teamQuery.query(tracker.class.Team, {}, (result) => {
_result = JSON.stringify(result, undefined, 2)
console.log('#RESULT 124', _result)
const t = new Map<Ref<Team>, Team>()
for (const r of result) {
t.set(r._id, r)
}
_teams = t
})
let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
const statusesQuery = createQuery()
statusesQuery.query(
tracker.class.IssueStatus,
{},
(statuses) => {
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
for (const s of statuses) {
const id = s.attachedTo as Ref<Team>
st.set(id, [...(st.get(id) ?? []), s])
}
issueStatuses = st
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
$: viewOptions = getViewOptions(viewlet)
</script>
<IssuesHeader {viewlets} {label} {space} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
@@ -80,7 +106,7 @@
</svelte:fragment>
<svelte:fragment slot="extra">
{#if viewlet}
<ViewOptionsButton viewOptionsKey={viewlet._id} config={viewOptionsConfig} />
<ViewletSettingButton bind:viewOptions {viewlet} />
{/if}
{#if asideFloat && $$slots.aside}
<div class="buttons-divider" />
@@ -99,8 +125,8 @@
<slot name="afterHeader" />
<FilterBar _class={tracker.class.Issue} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
<div class="flex w-full h-full clear-mins">
{#if viewlet}
<IssuesContent {viewlet} query={resultQuery} />
{#if viewlet && _teams && issueStatuses}
<IssuesContent {viewlet} query={resultQuery} {space} teams={_teams} {issueStatuses} {viewOptions} />
{/if}
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
@@ -17,7 +17,8 @@
import { Class, Doc, DocumentQuery, Lookup, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { Kanban, TypeState } from '@hcengineering/kanban'
import notification from '@hcengineering/notification'
import { createQuery } from '@hcengineering/presentation'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker'
import {
@@ -30,18 +31,19 @@
showPopup,
tooltip
} from '@hcengineering/ui'
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@hcengineering/view-resources'
import { ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
import {
focusStore,
ListSelectionProvider,
noCategory,
SelectDirection,
selectionStore
} from '@hcengineering/view-resources'
import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte'
import Menu from '@hcengineering/view-resources/src/components/Menu.svelte'
import { onMount } from 'svelte'
import tracker from '../../plugin'
import {
getIssueStatusStates,
getKanbanStatuses,
getPriorityStates,
issuesGroupBySorting,
issuesSortOrderMap
} from '../../utils'
import { getIssueStatusStates, getKanbanStatuses, getPriorityStates, issuesGroupBySorting } from '../../utils'
import CreateIssue from '../CreateIssue.svelte'
import ProjectEditor from '../projects/ProjectEditor.svelte'
import AssigneePresenter from './AssigneePresenter.svelte'
@@ -53,24 +55,17 @@
import StatusEditor from './StatusEditor.svelte'
import EstimationEditor from './timereport/EstimationEditor.svelte'
export let currentSpace: Ref<Team> = tracker.team.DefaultTeam
export let space: Ref<Team> | undefined = undefined
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let query: DocumentQuery<Issue> = {}
export let viewOptions: {
groupBy: IssuesGrouping
orderBy: IssuesOrdering
shouldShowEmptyGroups: boolean
shouldShowSubIssues: boolean
}
export let viewOptionsConfig: ViewOptionModel[] | undefined
export let viewOptions: ViewOptions
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
$: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions)
$: sort = { [orderBy]: issuesSortOrderMap[orderBy] }
$: rankFieldName = orderBy === IssuesOrdering.Manual ? orderBy : undefined
$: resultQuery = {
...(shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }),
...query
} as any
$: currentSpace = space || tracker.team.DefaultTeam
$: groupBy = (viewOptions.groupBy ?? noCategory) as IssuesGrouping
$: orderBy = viewOptions.orderBy
$: sort = { [orderBy[0]]: orderBy[1] }
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
const spaceQuery = createQuery()
const statusesQuery = createQuery()
@@ -80,6 +75,28 @@
currentTeam = res.shift()
})
let resultQuery: DocumentQuery<any> = query
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = p))
const client = getClient()
const hierarchy = client.getHierarchy()
async function getResultQuery (
query: DocumentQuery<Issue>,
viewOptions: ViewOptionModel[] | undefined,
viewOptionsStore: ViewOptions
): Promise<DocumentQuery<Issue>> {
if (viewOptions === undefined) return query
let result = hierarchy.clone(query)
for (const viewOption of viewOptions) {
if (viewOption.actionTartget !== 'query') continue
const queryOption = viewOption as ViewQueryOption
const f = await getResource(queryOption.action)
result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
}
return result
}
let issueStatuses: WithLookup<IssueStatus>[] | undefined
$: issueStatusStates = getIssueStatusStates(issueStatuses)
$: statusesQuery.query(
@@ -122,6 +139,12 @@
}
const issuesQuery = createQuery()
let issueStates: TypeState[] = []
const lookupIssue: Lookup<Issue> = {
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
project: tracker.class.Project,
sprint: tracker.class.Sprint,
assignee: contact.class.Employee
}
$: issuesQuery.query(
tracker.class.Issue,
resultQuery,
@@ -129,12 +152,7 @@
issueStates = await getKanbanStatuses(groupBy, result)
},
{
lookup: {
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
project: tracker.class.Project,
sprint: tracker.class.Sprint,
assignee: contact.class.Employee
},
lookup: lookupIssue,
sort: issuesGroupBySorting[groupBy]
}
)
@@ -145,17 +163,16 @@
})
function getIssueStates (
groupBy: IssuesGrouping,
showEmptyGroups: boolean,
states: TypeState[],
statusStates: TypeState[],
priorityStates: TypeState[]
) {
if (!showEmptyGroups && states.length > 0) return states
if (states.length > 0) return states
if (groupBy === IssuesGrouping.Status) return statusStates
if (groupBy === IssuesGrouping.Priority) return priorityStates
return []
}
$: states = getIssueStates(groupBy, shouldShowEmptyGroups, issueStates, issueStatusStates, priorityStates)
$: states = getIssueStates(groupBy, issueStates, issueStatusStates, priorityStates)
const fullFilled: { [key: string]: boolean } = {}
const getState = (state: any): WithLookup<IssueStatus> | undefined => {
@@ -171,15 +188,15 @@
mode: 'browser'
}}
/>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Kanban
bind:this={kanbanUI}
_class={tracker.class.Issue}
search=""
{states}
{dontUpdateRank}
options={{ sort, lookup }}
query={resultQuery}
fieldName={groupBy}
{rankFieldName}
on:content={(evt) => {
listProvider.update(evt.detail)
}}
@@ -202,18 +219,16 @@
<span class="lines-limit-2 ml-2">{state.title}</span>
<span class="counter ml-2 text-md">{count}</span>
</div>
{#if groupBy === IssuesGrouping.Status}
<div class="flex gap-1">
<Button
icon={IconAdd}
kind={'transparent'}
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
on:click={() => {
showPopup(CreateIssue, { space: currentSpace, status: state._id }, 'top')
}}
/>
</div>
{/if}
<div class="flex gap-1">
<Button
icon={IconAdd}
kind={'transparent'}
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
on:click={() => {
showPopup(CreateIssue, { space: currentSpace, [groupBy]: state._id }, 'top')
}}
/>
</div>
</div>
</div>
</svelte:fragment>
@@ -244,7 +259,7 @@
<AssigneePresenter
value={issue.$lookup?.assignee}
defaultClass={contact.class.Employee}
issueId={issue._id}
object={issue}
isEditable={true}
/>
<div class="flex-center mt-2">
@@ -252,8 +267,8 @@
</div>
</div>
<div class="buttons-group xsmall-gap states-bar">
{#if issue && issueStatuses && issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses} />
{#if issue && issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentTeam} />
{/if}
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
<ProjectEditor
@@ -1,83 +0,0 @@
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { Class, Doc, DocumentQuery, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, ViewOptions } from '@hcengineering/tracker'
import { issueSP, Scroller } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import tracker from '../../plugin'
import {
getCategories,
groupBy as groupByFunc,
issuesGroupKeyMap,
issuesOrderKeyMap,
issuesSortOrderMap
} from '../../utils'
import IssuesListBrowser from './IssuesListBrowser.svelte'
export let _class: Ref<Class<Doc>>
export let config: (string | BuildModelKey)[]
export let query: DocumentQuery<Issue> = {}
export let viewOptions: ViewOptions
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
$: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions)
$: groupByKey = issuesGroupKeyMap[groupBy]
$: orderByKey = issuesOrderKeyMap[orderBy]
$: subIssuesQuery = shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }
const statusesQuery = createQuery()
let statuses: IssueStatus[] = []
$: statusesQuery.query(
tracker.class.IssueStatus,
{ attachedTo: currentSpace },
(result) => {
statuses = [...result]
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
$: groupedIssues = groupByFunc(issues, groupBy)
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups, statuses, employees)
$: employees = issues.map((x) => x.$lookup?.assignee).filter(Boolean) as Employee[]
const issuesQuery = createQuery()
let issues: WithLookup<Issue>[] = []
$: issuesQuery.query(
tracker.class.Issue,
{ ...subIssuesQuery, ...query },
(result) => {
issues = result
},
{
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team,
sprint: tracker.class.Sprint,
_id: {
subIssues: tracker.class.Issue
}
}
}
)
</script>
<div class="w-full h-full clear-mins">
<Scroller fade={issueSP}>
<IssuesListBrowser
{_class}
{currentSpace}
{groupByKey}
orderBy={orderByKey}
{statuses}
{employees}
{categories}
itemsConfig={config}
{groupedIssues}
/>
</Scroller>
</div>
@@ -38,6 +38,7 @@
<Spinner size="small" />
{/if}
<span class="overflow-label issue-title">{issue.title}</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="button-close"
use:tooltip={{ label: tracker.string.RemoveParent, direction: 'bottom' }}
@@ -31,6 +31,7 @@
<div class="root" style:max-width={maxWidth}>
<span class="names">
{#each value.parents as parentInfo}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="name cursor-pointer" on:click={() => handleIssueEditorOpened(parentInfo)}
>{parentInfo.parentTitle}</span
>
@@ -73,6 +73,7 @@
{#if value}
{#if kind === 'list' || kind === 'list-header'}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="priority-container" on:click={handlePriorityEditorOpened}>
<div class="icon">
{#if issuePriorities[value.priority]?.icon}<Icon icon={issuePriorities[value.priority]?.icon} {size} />{/if}
@@ -0,0 +1,74 @@
<!--
// 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 { IssuePriority } from '@hcengineering/tracker'
import { Button, ButtonKind, ButtonSize, Icon, Label } from '@hcengineering/ui'
import { issuePriorities } from '../../utils'
export let value: IssuePriority
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
</script>
{#if kind === 'list' || kind === 'list-header'}
<div class="priority-container">
<div class="icon">
{#if issuePriorities[value]?.icon}<Icon icon={issuePriorities[value]?.icon} {size} />{/if}
</div>
<span
class="{kind === 'list' ? 'ml-2 text-md' : 'ml-3 text-base'} overflow-label disabled fs-bold content-accent-color"
>
<Label label={issuePriorities[value]?.label} />
</span>
</div>
{:else}
<Button
label={issuePriorities[value]?.label}
icon={issuePriorities[value]?.icon}
{justify}
{width}
{size}
{kind}
disabled
/>
{/if}
<style lang="scss">
.priority-container {
display: flex;
align-items: center;
flex-shrink: 0;
min-width: 0;
cursor: pointer;
.icon {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: 1rem;
height: 1rem;
color: var(--content-color);
}
&:hover {
.icon {
color: var(--caption-color) !important;
}
}
}
</style>
@@ -15,7 +15,7 @@
<script lang="ts">
import { AttachedData, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus } from '@hcengineering/tracker'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
@@ -34,6 +34,9 @@
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
// Extra properties
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]> | undefined = undefined
const client = getClient()
const statusesQuery = createQuery()
const dispatch = createEventDispatcher()
@@ -65,7 +68,7 @@
$: selectedStatus = statuses?.find((status) => status._id === value.status) ?? statuses?.[0]
$: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined
$: statusesInfo = statuses?.map((s, i) => {
$: statusesInfo = statuses?.map((s) => {
return {
id: s._id,
component: StatusPresenter,
@@ -74,18 +77,23 @@
}
})
$: if (!statuses) {
const query = '_id' in value ? { attachedTo: value.space } : {}
statusesQuery.query(
tracker.class.IssueStatus,
query,
(result) => {
statuses = result
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
statuses = '_id' in value ? issueStatuses?.get(value.space) : undefined
if (statuses === undefined) {
const query = '_id' in value ? { attachedTo: value.space } : {}
statusesQuery.query(
tracker.class.IssueStatus,
query,
(result) => {
statuses = result
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
} else {
statusesQuery.unsubscribe()
}
}
$: smallgap = size === 'inline' || size === 'small'
</script>
@@ -95,7 +103,11 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-row-center flex-no-shrink" class:cursor-pointer={isEditable} on:click={handleStatusEditorOpened}>
<div class="flex-center flex-no-shrink square-4">
{#if selectedStatus}<IssueStatusIcon value={selectedStatus} size={kind === 'list' ? 'inline' : 'medium'} />{/if}
{#if selectedStatus}<IssueStatusIcon
value={selectedStatus}
issueStatuses={statuses}
size={kind === 'list' ? 'inline' : 'medium'}
/>{/if}
</div>
{#if selectedStatusLabel}
<span
@@ -0,0 +1,32 @@
<!--
// 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 { Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import tracker, { IssueStatus } from '@hcengineering/tracker'
import StatusPresenter from './StatusPresenter.svelte'
export let value: Ref<IssueStatus> | undefined
export let size: 'small' | 'medium' = 'medium'
let status: WithLookup<IssueStatus> | undefined
const query = createQuery()
$: query.query(tracker.class.IssueStatus, { _id: value }, (res) => ([status] = res), {
lookup: { category: tracker.class.IssueStatusCategory }
})
</script>
<StatusPresenter value={status} {size} />
@@ -14,12 +14,13 @@
-->
<script lang="ts">
import type { Issue } from '@hcengineering/tracker'
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
import tracker from '../../plugin'
import { showPanel } from '@hcengineering/ui'
import tracker from '../../plugin'
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
export let value: Issue
export let shouldUseMargin: boolean = false
export let showParent = true
function handleIssueEditorOpened () {
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
@@ -28,12 +29,15 @@
{#if value}
<span class="titlePresenter-container" class:with-margin={shouldUseMargin} title={value.title}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="name overflow-label cursor-pointer"
style={`max-width: ${value.parents.length !== 0 ? 95 : 100}%`}
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
on:click={handleIssueEditorOpened}>{value.title}</span
>
<ParentNamesPresenter {value} />
{#if showParent}
<ParentNamesPresenter {value} />
{/if}
</span>
{/if}
@@ -20,7 +20,7 @@
import { getResource } from '@hcengineering/platform'
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting'
import type { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker'
import type { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import {
Button,
EditBox,
@@ -33,14 +33,7 @@
showPopup,
Spinner
} from '@hcengineering/ui'
import {
ContextMenu,
focusStore,
ListSelectionProvider,
SelectDirection,
UpDownNavigator,
viewOptionsStore
} from '@hcengineering/view-resources'
import { ContextMenu, UpDownNavigator } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues'
import tracker from '../../../plugin'
@@ -49,8 +42,6 @@
import CopyToClipboard from './CopyToClipboard.svelte'
import SubIssues from './SubIssues.svelte'
import SubIssueSelector from './SubIssueSelector.svelte'
import { groupBy as groupByFunc, issuesOrderKeyMap, issuesSortOrderMap } from '../../../utils'
import contact from '@hcengineering/contact'
export let _id: Ref<Issue>
export let _class: Ref<Class<Issue>>
@@ -71,14 +62,6 @@
let isEditing = false
let descriptionBox: AttachmentStyledBox
let groupBy: IssuesGrouping
let orderBy: IssuesOrdering
let shouldShowSubIssues: boolean
$: ({ groupBy, orderBy, shouldShowSubIssues } = $viewOptionsStore)
$: orderByKey = issuesOrderKeyMap[orderBy]
$: subIssuesQuery = shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }
$: query = { space: issue?.space }
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
$: read(_id)
@@ -126,72 +109,6 @@
$: isDescriptionEmpty = !new DOMParser().parseFromString(description, 'text/html').documentElement.innerText?.trim()
$: parentIssue = issue?.$lookup?.attachedTo
let issues: WithLookup<Issue>[] = []
let neighbourIssues: Issue[] = []
const issuesQuery = createQuery()
const subIssuesQueryClient = createQuery()
$: if (parentIssue) {
subIssuesQueryClient.query(
tracker.class.Issue,
{ attachedTo: parentIssue?._id },
async (result) => (neighbourIssues = result ?? []),
{
sort: { rank: SortingOrder.Descending }
}
)
} else {
issuesQuery.query(
tracker.class.Issue,
{ ...subIssuesQuery, ...query },
(result) => {
issues = result
},
{
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team,
sprint: tracker.class.Sprint,
_id: {
subIssues: tracker.class.Issue
}
}
}
)
}
$: groupedIssues = groupByFunc(issues, groupBy)
$: flatGroupedIssues = Object.values(groupedIssues ?? {}).flat(1)
$: issuesToNavigate = parentIssue ? neighbourIssues : flatGroupedIssues
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
if (groupedIssues) {
const selectedRowIndex = listProvider.current($focusStore)
let position =
(of !== undefined ? issuesToNavigate.findIndex((x) => x._id === of?._id) : selectedRowIndex) ?? -1
position -= offset
if (position < 0) {
position = 0
}
if (position >= issuesToNavigate.length) {
position = issuesToNavigate.length - 1
}
listProvider.updateFocus(issuesToNavigate[position])
}
}
})
$: if (issue) listProvider.updateFocus(issue)
$: listProvider.update(issuesToNavigate)
function edit (ev: MouseEvent) {
ev.preventDefault()
@@ -324,6 +241,7 @@
<span class="title select-text">{title}</span>
<div class="mt-6 description-preview select-text">
{#if isDescriptionEmpty}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="placeholder" on:click={edit}>
<Label label={tracker.string.IssueDescriptionPlaceholder} />
</div>
@@ -13,63 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { getEventPositionElement, showPanel, showPopup } from '@hcengineering/ui'
import { ActionContext, ContextMenu, FixedColumn } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import { getIssueId } from '../../../issues'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import { ActionContext, List } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import Circles from '../../icons/Circles.svelte'
import AssigneeEditor from '../AssigneeEditor.svelte'
import DueDateEditor from '../DueDateEditor.svelte'
import PriorityEditor from '../PriorityEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte'
import SubIssuesSelector from './SubIssuesSelector.svelte'
export let issues: Issue[]
export let query: DocumentQuery<Issue> | undefined = undefined
export let issues: Issue[] | undefined = undefined
export let viewlet: Viewlet
export let viewOptions: ViewOptions
export let teams: Map<Ref<Team>, Team>
// Extra properties
export let teams: Map<Ref<Team>, Team> | undefined
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
const dispatch = createEventDispatcher()
let draggingIndex: number | null = null
let hoveringIndex: number | null = null
function openIssue (target: Issue) {
dispatch('issue-focus', target)
showPanel(tracker.component.EditIssue, target._id, target._class, 'content')
}
function resetDrag () {
draggingIndex = null
hoveringIndex = null
}
function handleDragStart (ev: DragEvent, index: number) {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move'
ev.dataTransfer.dropEffect = 'move'
draggingIndex = index
}
}
function handleDrop (ev: DragEvent, toIndex: number) {
if (ev.dataTransfer && draggingIndex !== null && toIndex !== draggingIndex) {
ev.dataTransfer.dropEffect = 'move'
dispatch('move', { fromIndex: draggingIndex, toIndex })
}
resetDrag()
}
function showContextMenu (ev: MouseEvent, object: Issue) {
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
}
</script>
<ActionContext
@@ -78,136 +35,15 @@
}}
/>
{#each issues as issue, index (issue._id)}
{@const currentTeam = teams.get(issue.space)}
{@const openIssueCall = () => openIssue(issue)}
<div
class="flex-between row"
class:is-dragging={index === draggingIndex}
class:is-dragged-over-up={draggingIndex !== null && index < draggingIndex && index === hoveringIndex}
class:is-dragged-over-down={draggingIndex !== null && index > draggingIndex && index === hoveringIndex}
animate:flip={{ duration: 400 }}
draggable={true}
on:click|self={openIssueCall}
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
on:dragstart={(ev) => handleDragStart(ev, index)}
on:dragover|preventDefault={() => false}
on:dragenter={() => (hoveringIndex = index)}
on:drop|preventDefault={(ev) => handleDrop(ev, index)}
on:dragend={resetDrag}
>
<div class="draggable-container">
<div class="draggable-mark"><Circles /></div>
</div>
<div class="flex-row-center ml-6 clear-mins gap-2">
<PriorityEditor value={issue} isEditable kind={'list'} size={'small'} justify={'center'} />
<span class="issuePresenter" on:click={openIssueCall}>
<FixedColumn key={'subissue_issue'} justify={'left'}>
{#if currentTeam}
{getIssueId(currentTeam, issue)}
{/if}
</FixedColumn>
</span>
<StatusEditor
value={issue}
statuses={issueStatuses.get(issue.space)}
justify="center"
kind={'list'}
size={'small'}
tooltipAlignment="bottom"
/>
<span class="text name" title={issue.title} on:click={openIssueCall}>
{issue.title}
</span>
{#if issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses.get(issue.space)} />
{/if}
</div>
<div class="flex-center flex-no-shrink">
<EstimationEditor value={issue} kind={'list'} />
{#if issue.dueDate !== null}
<DueDateEditor value={issue} />
{/if}
<AssigneeEditor value={issue} />
</div>
</div>
{/each}
<style lang="scss">
.row {
position: relative;
border-bottom: 1px solid var(--divider-color);
.text {
font-weight: 500;
color: var(--caption-color);
}
.issuePresenter {
flex-shrink: 0;
min-width: 0;
min-height: 0;
font-weight: 500;
color: var(--content-color);
cursor: pointer;
&:hover {
color: var(--caption-color);
text-decoration: underline;
}
&:active {
color: var(--accent-color);
}
}
.name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.draggable-container {
position: absolute;
display: flex;
align-items: center;
height: 100%;
width: 1.5rem;
cursor: grabbing;
.draggable-mark {
opacity: 0;
width: 0.375rem;
height: 1rem;
margin-left: 0.75rem;
transition: opacity 0.1s;
}
}
&:hover {
.draggable-mark {
opacity: 0.4;
}
}
&.is-dragging::before {
position: absolute;
content: '';
background-color: var(--theme-bg-color);
opacity: 0.4;
inset: 0;
}
&.is-dragged-over-up::before {
position: absolute;
content: '';
inset: 0;
border-top: 1px solid var(--theme-bg-check);
}
&.is-dragged-over-down::before {
position: absolute;
content: '';
inset: 0;
border-bottom: 1px solid var(--theme-bg-check);
}
}
</style>
{#if viewlet}
<List
_class={tracker.class.Issue}
{viewOptions}
viewOptionsConfig={viewlet.viewOptions?.other}
config={viewlet.config}
documents={issues}
{query}
flatHeaders={true}
props={{ teams, issueStatuses }}
/>
{/if}
@@ -31,6 +31,7 @@
import tracker from '../../../plugin'
import { getIssueId } from '../../../issues'
import IssueStatusIcon from '../IssueStatusIcon.svelte'
import { ListSelectionProvider } from '@hcengineering/view-resources'
export let issue: WithLookup<Issue>
@@ -48,6 +49,7 @@
function openParentIssue () {
if (parentIssue) {
closeTooltip()
ListSelectionProvider.Pop()
openIssue(parentIssue._id)
}
}
@@ -90,7 +92,7 @@
$: areSubIssuesLoading = !subIssues
$: parentIssue = issue.$lookup?.attachedTo ? (issue.$lookup?.attachedTo as Issue) : null
$: if (parentIssue) {
$: if (parentIssue && parentIssue.subIssues > 0) {
subIssuesQeury.query(
tracker.class.Issue,
{ space: issue.space, attachedTo: parentIssue._id },
@@ -111,6 +113,7 @@
{#if parentIssue}
<div class="flex root">
<div class="item clear-mins">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-center parent-issue cursor-pointer"
use:tooltip={{ label: tracker.string.OpenParent, direction: 'bottom' }}
@@ -134,11 +137,12 @@
<Spinner size="small" />
</div>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={subIssuesElement}
class="flex-center sub-issues cursor-pointer"
use:tooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }}
on:click|preventDefault={areSubIssuesLoading ? undefined : showSubIssues}
on:click|preventDefault={showSubIssues}
>
<span class="overflow-label">{subIssues?.length}</span>
<div class="ml-2">
@@ -14,12 +14,12 @@
-->
<script lang="ts">
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Button, Spinner, ExpandCollapse, closeTooltip, IconAdd } from '@hcengineering/ui'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Button, Chevron, closeTooltip, ExpandCollapse, IconAdd, Label } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import Collapsed from '../../icons/Collapsed.svelte'
import Expanded from '../../icons/Expanded.svelte'
import CreateSubIssue from './CreateSubIssue.svelte'
import SubIssueList from './SubIssueList.svelte'
@@ -27,84 +27,109 @@
export let teams: Map<Ref<Team>, Team>
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
const subIssuesQuery = createQuery()
const client = getClient()
let subIssues: Issue[] | undefined
let isCollapsed = false
let isCreating = false
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
if (subIssues) {
const { fromIndex, toIndex } = ev.detail
const [prev, next] = [
subIssues[fromIndex < toIndex ? toIndex : toIndex - 1],
subIssues[fromIndex < toIndex ? toIndex + 1 : toIndex]
]
const issue = subIssues[fromIndex]
$: hasSubIssues = issue.subIssues > 0
await client.update(issue, { rank: calcRank(prev, next) })
}
let viewlet: Viewlet | undefined
const query = createQuery()
$: query.query(view.class.Viewlet, { _id: tracker.viewlet.SubIssues }, (res) => {
;[viewlet] = res
})
let _teams = teams
let _issueStatuses = issueStatuses
const teamsQuery = createQuery()
$: if (teams === undefined) {
teamsQuery.query(tracker.class.Team, {}, async (result) => {
_teams = new Map(result.map((it) => [it._id, it]))
})
} else {
teamsQuery.unsubscribe()
}
$: hasSubIssues = issue.subIssues > 0
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
sort: { rank: SortingOrder.Ascending },
lookup: {
_id: {
subIssues: tracker.class.Issue
const statusesQuery = createQuery()
$: if (issueStatuses === undefined) {
statusesQuery.query(
tracker.class.IssueStatus,
{},
(statuses) => {
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
for (const s of statuses) {
const id = s.attachedTo as Ref<Team>
st.set(id, [...(st.get(id) ?? []), s])
}
_issueStatuses = st
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
}
})
)
} else {
statusesQuery.unsubscribe()
}
$: viewOptions = viewlet !== undefined ? getViewOptions(viewlet) : undefined
</script>
<div class="flex-between">
{#if hasSubIssues}
<Button
width="min-content"
icon={isCollapsed ? Collapsed : Expanded}
size="small"
kind="transparent"
label={tracker.string.SubIssuesList}
labelParams={{ subIssues: issue.subIssues }}
on:click={() => {
isCollapsed = !isCollapsed
isCreating = false
}}
/>
>
<svelte:fragment slot="content">
<Chevron size={'small'} expanded={!isCollapsed} outline fill={'var(--caption-color)'} marginRight={'.375rem'} />
<Label label={tracker.string.SubIssuesList} params={{ subIssues: issue.subIssues }} />
</svelte:fragment>
</Button>
{/if}
<Button
id="add-sub-issue"
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
on:click={() => {
closeTooltip()
isCreating = true
isCollapsed = false
}}
/>
<div class="flex-row-center">
{#if viewlet && hasSubIssues && viewOptions}
<ViewletSettingButton bind:viewOptions {viewlet} kind={'transparent'} />
{/if}
<Button
id="add-sub-issue"
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
on:click={() => {
closeTooltip()
isCreating = true
isCollapsed = false
}}
/>
</div>
</div>
<div class="mt-1">
{#if subIssues && issueStatuses}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
{#if hasSubIssues}
{#if issueStatuses}
{#if hasSubIssues && viewOptions && viewlet}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
<div class="list" class:collapsed={isCollapsed}>
<SubIssueList
issues={subIssues}
{issueStatuses}
{teams}
on:issue-focus={() => (isCreating = false)}
on:move={handleIssueSwap}
teams={_teams}
{viewlet}
{viewOptions}
issueStatuses={_issueStatuses}
query={{ attachedTo: issue._id }}
/>
</div>
{/if}
</ExpandCollapse>
</ExpandCollapse>
{/if}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
{#if isCreating}
{@const team = teams.get(issue.space)}
@@ -121,10 +146,6 @@
{/if}
{/if}
</ExpandCollapse>
{:else}
<div class="flex-center pt-3">
<Spinner />
</div>
{/if}
</div>
@@ -13,17 +13,26 @@
// limitations under the License.
-->
<script lang="ts">
import { Doc, Ref, WithLookup } from '@hcengineering/core'
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { ButtonKind, ButtonSize, getPlatformColor } from '@hcengineering/ui'
import { Button, closeTooltip, ProgressCircle, SelectPopup, showPanel, showPopup } from '@hcengineering/ui'
import { updateFocus } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import {
Button,
ButtonKind,
ButtonSize,
closeTooltip,
getPlatformColor,
ProgressCircle,
SelectPopup,
showPanel,
showPopup
} from '@hcengineering/ui'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import { subIssueListProvider } from '../../../utils'
export let value: WithLookup<Issue>
export let currentTeam: Team | undefined
export let statuses: WithLookup<IssueStatus>[] | undefined
export let kind: ButtonKind = 'link-bordered'
export let size: ButtonSize = 'inline'
@@ -32,21 +41,35 @@
let btn: HTMLElement
let subIssues: Issue[] | undefined
let doneStatus: Ref<Doc> | undefined
let subIssues: Issue[] = []
let countComplate: number = 0
const query = createQuery()
const statusesQuery = createQuery()
let statuses: WithLookup<IssueStatus>[] = []
$: if (value.$lookup?.subIssues !== undefined) {
query.unsubscribe()
subIssues = value.$lookup.subIssues as Issue[]
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
} else {
query.query(tracker.class.Issue, { attachedTo: value._id }, (res) => (subIssues = res), {
sort: { rank: SortingOrder.Ascending }
})
}
statusesQuery.query(tracker.class.IssueStatus, {}, (res) => (statuses = res), {
lookup: { category: tracker.class.IssueStatusCategory }
})
$: if (statuses && subIssues) {
doneStatus = statuses.find((s) => s.category === tracker.issueStatusCategory.Completed)?._id ?? undefined
if (doneStatus) countComplate = subIssues.filter((si) => si.status === doneStatus).length
const doneStatuses = statuses.filter((s) => s.category === tracker.issueStatusCategory.Completed).map((p) => p._id)
countComplate = subIssues.filter((si) => doneStatuses.includes(si.status)).length
}
$: hasSubIssues = (subIssues?.length ?? 0) > 0
function getIssueStatusIcon (issue: Issue) {
function getIssueStatusIcon (issue: Issue, statuses: WithLookup<IssueStatus>[] | undefined) {
const status = statuses?.find((s) => issue.status === s._id)
const category = status?.$lookup?.category
const color = status?.color ?? category?.color
@@ -59,9 +82,11 @@
function openIssue (target: Ref<Issue>) {
if (target !== value._id) {
subIssueListProvider(subIssues, target)
showPanel(tracker.component.EditIssue, target, value._class, 'content')
}
}
function showSubIssues () {
if (subIssues) {
closeTooltip()
@@ -71,7 +96,7 @@
value: subIssues.map((iss) => {
const text = currentTeam ? `${getIssueId(currentTeam, iss)} ${iss.title}` : iss.title
return { id: iss._id, text, isSelected: iss._id === value._id, ...getIssueStatusIcon(iss) }
return { id: iss._id, text, isSelected: iss._id === value._id, ...getIssueStatusIcon(iss, statuses) }
}),
width: 'large'
},
@@ -86,12 +111,6 @@
},
(selectedIssue) => {
selectedIssue !== undefined && openIssue(selectedIssue)
},
(selectedIssue) => {
const focus = subIssues?.find((it) => it._id === selectedIssue.id)
if (focus !== undefined) {
updateFocus({ focus })
}
}
)
}
@@ -17,10 +17,13 @@
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Label, Spinner } from '@hcengineering/ui'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import tracker from '../../../plugin'
import SubIssueList from '../edit/SubIssueList.svelte'
export let object: Doc
export let viewlet: Viewlet
export let viewOptions: ViewOptions
let query: DocumentQuery<Issue>
$: query = { 'relations._id': object._id, 'relations._class': object._class }
@@ -75,9 +78,9 @@
</script>
<div class="mt-1">
{#if subIssues !== undefined}
{#if subIssues !== undefined && viewlet !== undefined}
{#if issueStatuses.size > 0 && teams}
<SubIssueList issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
<SubIssueList bind:viewOptions {viewlet} issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
{:else}
<div class="p-1">
<Label label={presentation.string.NoMatchesFound} />
@@ -0,0 +1,54 @@
<script lang="ts">
import { Doc } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Button, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import RelatedIssues from './RelatedIssues.svelte'
export let object: Doc
export let label: IntlString
let viewlet: Viewlet | undefined
const vquery = createQuery()
$: vquery.query(view.class.Viewlet, { _id: tracker.viewlet.SubIssues }, (res) => {
;[viewlet] = res
})
let viewOptions = getViewOptions(viewlet)
</script>
<div class="antiSection">
<div class="antiSection-header">
<div class="antiSection-header__icon">
<Icon icon={tracker.icon.Issue} size={'small'} />
</div>
<span class="antiSection-header__title">
<Label {label} />
</span>
<div class="buttons-group small-gap">
{#if viewlet && viewOptions}
<ViewletSettingButton bind:viewOptions {viewlet} kind={'transparent'} />
{/if}
<Button
id="add-sub-issue"
width="min-content"
icon={IconAdd}
label={undefined}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
on:click={() => {
showPopup(tracker.component.CreateIssue, { relatedTo: object, space: object.space }, 'top')
}}
/>
</div>
</div>
<div class="flex-row">
{#if viewlet}
<RelatedIssues {object} {viewOptions} {viewlet} />
{/if}
</div>
</div>
@@ -120,9 +120,6 @@
<IssuePresenter value={object} disableClick />
</svelte:fragment>
<div class="header no-border flex-col p-1">
<div class="flex-row-center flex-between" />
</div>
{#if currentTeam && issueStatuses}
<SubIssuesEstimations
issue={object}
@@ -142,14 +139,18 @@
<Button
icon={IconAdd}
size={'small'}
on:click={(event) => {
showPopup(TimeSpendReportPopup, {
issueId: object._id,
issueClass: object._class,
space: object.space,
assignee: object.assignee,
defaultTimeReportDay
})
on:click={() => {
showPopup(
TimeSpendReportPopup,
{
issueId: object._id,
issueClass: object._class,
space: object.space,
assignee: object.assignee,
defaultTimeReportDay
},
'top'
)
}}
label={tracker.string.TimeSpendReportAdd}
/>
@@ -14,11 +14,11 @@
-->
<script lang="ts">
import contact from '@hcengineering/contact'
import { Doc, Ref } from '@hcengineering/core'
import { Ref } from '@hcengineering/core'
import { UserBox } from '@hcengineering/presentation'
import { Issue, Team } from '@hcengineering/tracker'
import { getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { deviceOptionsStore as deviceInfo, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import EstimationEditor from './EstimationEditor.svelte'
@@ -28,18 +28,19 @@
export let teams: Map<Ref<Team>, Team>
function showContextMenu (ev: MouseEvent, object: Issue) {
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
showPopup(ContextMenu, { object }, $deviceInfo.isMobile ? 'top' : getEventPositionElement(ev))
}
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
const listProvider = new ListSelectionProvider(() => {})
$: twoRows = $deviceInfo.twoRows
</script>
<ListView count={issues.length}>
<ListView count={issues.length} addClass={'step-tb-2-accent'}>
<svelte:fragment slot="item" let:item>
{@const issue = issues[item]}
{@const currentTeam = teams.get(issue.space)}
<div
class="flex-between row"
class="{twoRows ? 'flex-col' : 'flex-between'} p-text-2"
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
on:mouseover={() => {
listProvider.updateFocus(issue)
@@ -48,55 +49,32 @@
listProvider.updateFocus(issue)
}}
>
<div class="flex-row-center clear-mins gap-2 p-2 flex-grow">
<span class="issuePresenter">
<FixedColumn key={'estimation_issue'} justify={'left'}>
{#if currentTeam}
{getIssueId(currentTeam, issue)}
{/if}
</FixedColumn>
</span>
<span class="text name" title={issue.title}>
<div class="flex-row-center clear-mins gap-2 flex-grow mr-4" class:p-text={twoRows}>
<FixedColumn key={'estimation_issue'} justify={'left'} addClass={'fs-bold'}>
{#if currentTeam}
{getIssueId(currentTeam, issue)}
{/if}
</FixedColumn>
<span class="overflow-label fs-bold caption-color" title={issue.title}>
{issue.title}
</span>
</div>
<FixedColumn key={'estimation_issue_assignee'} justify={'right'}>
<UserBox
width={'100%'}
label={tracker.string.Assignee}
_class={contact.class.Employee}
value={issue.assignee}
readonly
showNavigate={false}
/>
</FixedColumn>
<FixedColumn key={'estimation'} justify={'left'}>
<EstimationEditor value={issue} kind={'list'} />
</FixedColumn>
<div class="flex-row-center clear-mins gap-2 self-end" class:p-text={twoRows}>
<FixedColumn key={'estimation_issue_assignee'} justify={'right'}>
<UserBox
width={'100%'}
label={tracker.string.Assignee}
_class={contact.class.Employee}
value={issue.assignee}
readonly
showNavigate={false}
/>
</FixedColumn>
<FixedColumn key={'estimation'} justify={'left'}>
<EstimationEditor value={issue} kind={'list'} />
</FixedColumn>
</div>
</div>
</svelte:fragment>
</ListView>
<style lang="scss">
.row {
.text {
font-weight: 500;
color: var(--caption-color);
}
.issuePresenter {
flex-shrink: 0;
min-width: 0;
min-height: 0;
font-weight: 500;
color: var(--content-color);
}
.name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
</style>
@@ -53,6 +53,7 @@
</script>
{#if kind === 'link'}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div id="ReportedTimeEditor" class="link-container flex-between" on:click={showReports}>
{#if value !== undefined}
<span class="overflow-label">
@@ -70,7 +70,7 @@
config={[
'$lookup.attachedTo',
'',
'$lookup.employee',
'employee',
{
key: '$lookup.attachedTo',
presenter: ParentNamesPresenter,
@@ -16,7 +16,7 @@
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Scroller, Spinner } from '@hcengineering/ui'
import { Spinner } from '@hcengineering/ui'
import Expandable from '@hcengineering/ui/src/components/Expandable.svelte'
import tracker from '../../../plugin'
import EstimationSubIssueList from './EstimationSubIssueList.svelte'
@@ -38,15 +38,9 @@
{#if subIssues && issueStatuses}
{#if hasSubIssues}
<Expandable label={tracker.string.ChildEstimation}>
<svelte:fragment slot="title">
: {total}
</svelte:fragment>
<div class="h-50">
<Scroller>
<EstimationSubIssueList issues={subIssues} {teams} />
</Scroller>
</div>
<Expandable label={tracker.string.ChildEstimation} contentColor bordered>
<svelte:fragment slot="title">: <span class="caption-color">{total}</span></svelte:fragment>
<EstimationSubIssueList issues={subIssues} {teams} />
</Expandable>
{/if}
{:else}
@@ -23,6 +23,7 @@
export let value: number
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
{id}
class:link={kind === 'link'}
@@ -15,11 +15,13 @@
<script lang="ts">
import { TimeReportDayType } from '@hcengineering/tracker'
import { DropdownIntlItem, DropdownLabelsIntl } from '@hcengineering/ui'
import type { ButtonKind } from '@hcengineering/ui'
import tracker from '../../../plugin'
import TimeReportDayIcon from './TimeReportDayIcon.svelte'
export let label = tracker.string.TimeReportDayTypeLabel
export let selected: TimeReportDayType | undefined
export let kind: ButtonKind = 'link-bordered'
const workDaysDropdownItems: DropdownIntlItem[] = [
{
@@ -34,7 +36,7 @@
</script>
<DropdownLabelsIntl
kind="link-bordered"
{kind}
icon={TimeReportDayIcon}
shouldUpdateUndefined={false}
{label}
@@ -18,7 +18,7 @@
import type { IntlString } from '@hcengineering/platform'
import presentation, { Card, getClient, UserBox } from '@hcengineering/presentation'
import { Issue, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
import { DatePresenter, EditBox } from '@hcengineering/ui'
import { DatePresenter, EditBox, Button } from '@hcengineering/ui'
import tracker from '../../../plugin'
import { getTimeReportDate, getTimeReportDayType } from '../../../utils'
import TimeReportDayDropdown from './TimeReportDayDropdown.svelte'
@@ -85,23 +85,31 @@
>
<div class="flex-row-center gap-2">
<EditBox focus bind:value={data.value} {placeholder} format={'number'} maxDigitsAfterPoint={3} kind={'editbox'} />
<Button kind={'link-bordered'} on:click={() => (data.value = 0.125)}><span slot="content">1/8</span></Button>
<Button kind={'link-bordered'} on:click={() => (data.value = 0.25)}><span slot="content">1/4</span></Button>
<Button kind={'link-bordered'} on:click={() => (data.value = 0.5)}><span slot="content">1/2</span></Button>
<Button kind={'link-bordered'} on:click={() => (data.value = 0.75)}><span slot="content">3/4</span></Button>
<div class="buttons-divider" />
<Button kind={'link-bordered'} on:click={() => (data.value = 1)}><span slot="content">1</span></Button>
</div>
<EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} />
<svelte:fragment slot="pool">
<UserBox
_class={contact.class.Employee}
label={contact.string.Employee}
kind={'link-bordered'}
kind={'no-border'}
bind:value={data.employee}
showNavigate={false}
/>
<TimeReportDayDropdown
kind={'no-border'}
bind:selected={selectedTimeReportDay}
on:selected={({ detail }) => (data.date = getTimeReportDate(detail))}
/>
<DatePresenter
kind={'link'}
bind:value={data.date}
editable
on:change={({ detail }) => (selectedTimeReportDay = getTimeReportDayType(detail))}
/>
</div>
<EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} />
</svelte:fragment>
</Card>
@@ -16,7 +16,7 @@
import { DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, Team, TimeSpendReport } from '@hcengineering/tracker'
import { Expandable, floorFractionDigits, Label, Scroller, Spinner } from '@hcengineering/ui'
import { Expandable, floorFractionDigits, Label, Spinner } from '@hcengineering/ui'
import tracker from '../../../plugin'
import TimePresenter from './TimePresenter.svelte'
import TimeSpendReportsList from './TimeSpendReportsList.svelte'
@@ -42,21 +42,19 @@
</script>
{#if reports}
<Expandable expanded={true}>
<Expandable expanded={true} contentColor bordered>
<svelte:fragment slot="title">
<span class="overflow-label flex-nowrap">
<Label label={tracker.string.ReportedTime} />: <TimePresenter value={reportedTime} {workDayLength} />
<Label label={tracker.string.TimeSpendReports} />: <TimePresenter value={total} {workDayLength} />
<Label label={tracker.string.ReportedTime} />:
<span class="caption-color"><TimePresenter value={reportedTime} {workDayLength} /></span>.
<Label label={tracker.string.TimeSpendReports} />:
<span class="caption-color"><TimePresenter value={total} {workDayLength} /></span>
</span>
</svelte:fragment>
<div class="h-50">
<Scroller>
<TimeSpendReportsList {reports} {teams} />
</Scroller>
</div>
<TimeSpendReportsList {reports} {teams} />
</Expandable>
{:else}
<div class="flex-center pt-3">
<div class="flex-center">
<Spinner />
</div>
{/if}
@@ -14,12 +14,18 @@
-->
<script lang="ts">
import contact from '@hcengineering/contact'
import { Doc, Ref, Space, WithLookup } from '@hcengineering/core'
import { Ref, Space, WithLookup } from '@hcengineering/core'
import UserBox from '@hcengineering/presentation/src/components/UserBox.svelte'
import { Team, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
import { eventToHTMLElement, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
import {
deviceOptionsStore as deviceInfo,
eventToHTMLElement,
getEventPositionElement,
ListView,
showPopup
} from '@hcengineering/ui'
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import TimePresenter from './TimePresenter.svelte'
@@ -33,7 +39,7 @@
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
}
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
const listProvider = new ListSelectionProvider(() => {})
const toTeamId = (ref: Ref<Space>) => ref as Ref<Team>
@@ -51,18 +57,19 @@
assignee: value.employee,
defaultTimeReportDay
},
eventToHTMLElement(event)
$deviceInfo.isMobile ? 'top' : eventToHTMLElement(event)
)
}
$: twoRows = $deviceInfo.twoRows
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<ListView count={reports.length}>
<ListView count={reports.length} addClass={'step-tb-2-accent'}>
<svelte:fragment slot="item" let:item>
{@const report = reports[item]}
{@const currentTeam = teams.get(toTeamId(report.space))}
<div
class="flex-between row"
class="{twoRows ? 'flex-col' : 'flex-between'} p-text-2"
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, report)}
on:mouseover={() => {
listProvider.updateFocus(report)
@@ -72,62 +79,36 @@
}}
on:click={(evt) => editSpendReport(evt, report, currentTeam?.defaultTimeReportDay)}
>
<div class="flex-row-center clear-mins gap-2 p-2 flex-grow">
<span class="issuePresenter">
<FixedColumn key={'tmiespend_issue'} justify={'left'}>
{#if currentTeam && report.$lookup?.attachedTo}
{getIssueId(currentTeam, report.$lookup?.attachedTo)}
{/if}
</FixedColumn>
</span>
<div class="flex-row-center clear-mins gap-2 flex-grow mr-4" class:p-text={twoRows}>
<FixedColumn key={'tmiespend_issue'} justify={'left'} addClass={'fs-bold'}>
{#if currentTeam && report.$lookup?.attachedTo}
{getIssueId(currentTeam, report.$lookup?.attachedTo)}
{/if}
</FixedColumn>
{#if report.$lookup?.attachedTo?.title}
<span class="text name" title={report.$lookup?.attachedTo?.title}>
<span class="overflow-label fs-bold caption-color" title={report.$lookup?.attachedTo?.title}>
{report.$lookup?.attachedTo?.title}
</span>
{/if}
</div>
<FixedColumn key={'timespend_assignee'} justify={'left'}>
<UserBox
width={'100%'}
label={tracker.string.Assignee}
_class={contact.class.Employee}
value={report.employee}
readonly
showNavigate={false}
/>
</FixedColumn>
<FixedColumn key={'timespend_reported'} justify={'center'}>
<div class="p-1">
<div class="flex-row-center clear-mins gap-2 self-end" class:p-text={twoRows}>
<FixedColumn key={'timespend_assignee'} justify={'left'}>
<UserBox
width={'100%'}
label={tracker.string.Assignee}
_class={contact.class.Employee}
value={report.employee}
readonly
showNavigate={false}
/>
</FixedColumn>
<FixedColumn key={'timespend_reported'} justify={'center'}>
<TimePresenter value={report.value} workDayLength={currentTeam?.workDayLength} />
</div>
</FixedColumn>
<FixedColumn key={'timespend_date'} justify={'left'}>
<DatePresenter value={report.date} />
</FixedColumn>
</div>
</svelte:fragment>
</FixedColumn>
<FixedColumn key={'timespend_date'} justify={'left'}>
<DatePresenter value={report.date} />
</FixedColumn>
</div>
</div></svelte:fragment
>
</ListView>
<style lang="scss">
.row {
.text {
font-weight: 500;
color: var(--caption-color);
}
.issuePresenter {
flex-shrink: 0;
min-width: 0;
min-height: 0;
font-weight: 500;
color: var(--content-color);
}
.name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
</style>