mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-12 12:47:45 +02:00
Tracker: Issues list view (#1313)
Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
<script lang='ts'>
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { IssueStatus, Team } from '@anticrm/tracker'
|
||||
import Issues from './Issues.svelte'
|
||||
export let currentSpace: Ref<Team>
|
||||
import tracker from '../../plugin'
|
||||
|
||||
let todalIssues = 0
|
||||
export let currentSpace: Ref<Team>
|
||||
</script>
|
||||
<div class='fs-title'>
|
||||
Active issues {todalIssues}
|
||||
</div>
|
||||
<Issues currentSpace={currentSpace} categories={[IssueStatus.InProgress, IssueStatus.Todo]} on:content={(evt) => { todalIssues = evt.detail.length } }></Issues>
|
||||
|
||||
<Issues {currentSpace} categories={[IssueStatus.InProgress, IssueStatus.Todo]} title={tracker.string.ActiveIssues} />
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang='ts'>
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { IssueStatus, Team } from '@anticrm/tracker'
|
||||
import Issues from './Issues.svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
</script>
|
||||
<Issues {currentSpace} categories={[IssueStatus.Backlog]}></Issues>
|
||||
|
||||
<Issues title={tracker.string.BacklogIssues} {currentSpace} categories={[IssueStatus.Backlog]} />
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
<div class='flex-between mb-2'>
|
||||
<IssuePresenter value={object} {currentTeam}/>
|
||||
{#if issue.$lookup?.assignee }
|
||||
<Component is={view.component.ObjectPresenter} props={{ value: issue.$lookup.assignee, props: { showLabel: false } }}/>
|
||||
<Component is={view.component.ObjectPresenter} props={{ value: issue.$lookup.assignee, props: { shouldShowName: false } }}/>
|
||||
{/if}
|
||||
</div>
|
||||
<span class='fs-bold title'>
|
||||
|
||||
@@ -1,52 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { DocumentQuery, FindOptions } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Scroller } from '@anticrm/ui'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import contact from '@anticrm/contact'
|
||||
import { DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Icon, IconAdd, Scroller, Tooltip, Button, showPopup, Label } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import IssuesList from './IssuesList.svelte'
|
||||
import { issueStatuses } from '../../utils'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
|
||||
export let query: DocumentQuery<Issue>
|
||||
export let category: IssueStatus
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let currentTeam: Team
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const options: FindOptions<Issue> = {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee
|
||||
}
|
||||
}
|
||||
|
||||
let visible = false
|
||||
let issuesAmount = 0
|
||||
|
||||
const handleNewIssueAdded = (event: Event) => {
|
||||
if (!currentSpace) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(CreateIssue, { space: currentSpace, issueStatus: category }, event.target)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='category' class:visible={visible}>
|
||||
<div class='fs-title'>
|
||||
{IssueStatus[category]}
|
||||
<div class="category" class:visible={issuesAmount > 0}>
|
||||
<div class="header categoryHeader flex-between label">
|
||||
<div class="flex-row-center gap-2">
|
||||
<Icon icon={issueStatuses[category].icon} size={'small'} />
|
||||
<span class="lines-limit-2"><Label label={issueStatuses[category].label} /></span>
|
||||
<span class="eLabelCounter ml-2">{issuesAmount}</span>
|
||||
</div>
|
||||
<div class="flex mr-1">
|
||||
<Tooltip label={tracker.string.AddIssueTooltip} direction={'left'}>
|
||||
<Button icon={IconAdd} kind={'transparent'} on:click={handleNewIssueAdded} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Scroller>
|
||||
<Table
|
||||
_class={tracker.class.Issue}
|
||||
config={[
|
||||
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam }, label: tracker.string.Issue },
|
||||
'title',
|
||||
// 'status',
|
||||
'$lookup.assignee',
|
||||
'modifiedOn'
|
||||
]}
|
||||
options={options}
|
||||
query={{ ...query, status: category }}
|
||||
showNotification
|
||||
highlightRows
|
||||
on:content={(evt) => { visible = evt.detail.length > 0 }}
|
||||
/>
|
||||
</Scroller>
|
||||
<Scroller>
|
||||
<IssuesList
|
||||
_class={tracker.class.Issue}
|
||||
config={[
|
||||
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
|
||||
{ key: '', presenter: tracker.component.TitlePresenter },
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
||||
{
|
||||
key: '$lookup.assignee',
|
||||
props: {
|
||||
shouldShowName: false,
|
||||
shouldShowPlaceholder: true,
|
||||
tooltipLabels: { personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }
|
||||
}
|
||||
}
|
||||
]}
|
||||
{options}
|
||||
query={{ ...query, status: category }}
|
||||
on:content={(evt) => {
|
||||
issuesAmount = evt.detail.length
|
||||
dispatch('content', issuesAmount)
|
||||
}}
|
||||
/>
|
||||
</Scroller>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.category {
|
||||
display: none;
|
||||
&.visible {
|
||||
display: block;
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryHeader {
|
||||
height: 2.5rem;
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: var(--theme-caption-color);
|
||||
.eLabelCounter {
|
||||
opacity: 0.8;
|
||||
font-weight: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import type { Issue, Team } from '@anticrm/tracker'
|
||||
import { Icon, showPanel } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { issuePriorities } from '../../utils'
|
||||
|
||||
export let value: Issue
|
||||
export let currentTeam: Team
|
||||
@@ -37,6 +38,9 @@
|
||||
class:inline-presenter={inline}
|
||||
on:click={show}
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon icon={issuePriorities[value.priority].icon} size={'small'} />
|
||||
</div>
|
||||
<div class="icon">
|
||||
<Icon icon={tracker.icon.Issue} size={'small'} />
|
||||
</div>
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
import type { DocumentQuery, Ref } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { ScrollBox } from '@anticrm/ui'
|
||||
import { Label, ScrollBox } from '@anticrm/ui'
|
||||
import CategoryPresenter from './CategoryPresenter.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
export let categories = [
|
||||
@@ -28,15 +29,26 @@
|
||||
IssueStatus.Done,
|
||||
IssueStatus.Canceled
|
||||
]
|
||||
|
||||
export let title: IntlString = tracker.string.AllIssues
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let search: string = ''
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
const issuesMap: { [status: string]: number } = {}
|
||||
|
||||
$: getTotalIssues = () => {
|
||||
let total = 0
|
||||
|
||||
for (const issuesAmount of Object.values(issuesMap)) {
|
||||
total += issuesAmount
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
$: resultQuery =
|
||||
search === '' ? { space: currentSpace, ...query } : { $search: search, space: currentSpace, ...query }
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
|
||||
let currentTeam: Team | undefined
|
||||
|
||||
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
||||
@@ -46,9 +58,21 @@
|
||||
|
||||
{#if currentTeam}
|
||||
<ScrollBox vertical stretch>
|
||||
<div class="fs-title">
|
||||
<Label label={title} params={{ value: getTotalIssues() }} />
|
||||
</div>
|
||||
|
||||
<div class="ml-4 mt-4">
|
||||
{#each categories as category}
|
||||
<CategoryPresenter {category} query={resultQuery} {currentTeam} />
|
||||
<CategoryPresenter
|
||||
{category}
|
||||
query={resultQuery}
|
||||
{currentSpace}
|
||||
{currentTeam}
|
||||
on:content={(event) => {
|
||||
issuesMap[category] = event.detail
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</ScrollBox>
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
<!--
|
||||
// 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 { Class, Doc, DocumentQuery, FindOptions, Ref, getObjectValue } from '@anticrm/core'
|
||||
import { SortingOrder } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { CheckBox, Loading, showPopup, Spinner, IconMoreV } from '@anticrm/ui'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { buildModel, LoadingProps, Menu } from '@anticrm/view-resources'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let config: (BuildModelKey | string)[]
|
||||
export let options: FindOptions<Doc> | undefined = undefined
|
||||
export let query: DocumentQuery<Doc>
|
||||
|
||||
// If defined, will show a number of dummy items before real data will appear.
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const DOCS_MAX_AMOUNT = 200
|
||||
const liveQuery = createQuery()
|
||||
const sort = { modifiedOn: SortingOrder.Descending }
|
||||
|
||||
let selectedIssueIds = new Set<Ref<Doc>>()
|
||||
let selectedRowIndex: number | undefined
|
||||
let isLoading = false
|
||||
let docObjects: Doc[] | undefined
|
||||
let queryIndex = 0
|
||||
|
||||
const updateData = async (_class: Ref<Class<Doc>>, query: DocumentQuery<Doc>, options?: FindOptions<Doc>) => {
|
||||
const i = ++queryIndex
|
||||
|
||||
isLoading = true
|
||||
|
||||
liveQuery.query(
|
||||
_class,
|
||||
query,
|
||||
(result) => {
|
||||
if (i !== queryIndex) {
|
||||
return // our data is invalid.
|
||||
}
|
||||
|
||||
docObjects = result
|
||||
dispatch('content', docObjects)
|
||||
isLoading = false
|
||||
},
|
||||
{ sort, ...options, limit: DOCS_MAX_AMOUNT }
|
||||
)
|
||||
}
|
||||
|
||||
$: updateData(_class, query, options)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const showMenu = async (event: MouseEvent, docObject: Doc, rowIndex: number) => {
|
||||
selectedRowIndex = rowIndex
|
||||
|
||||
showPopup(Menu, { object: docObject, baseMenuClass }, event.target as HTMLElement, () => {
|
||||
selectedRowIndex = undefined
|
||||
})
|
||||
}
|
||||
|
||||
const handleIssueSelected = (id: Ref<Doc>, event: Event) => {
|
||||
const eventTarget = event.target as HTMLInputElement
|
||||
const isChecked = eventTarget.checked
|
||||
|
||||
if (isChecked) {
|
||||
selectedIssueIds.add(id)
|
||||
} else {
|
||||
selectedIssueIds.delete(id)
|
||||
}
|
||||
|
||||
selectedIssueIds = selectedIssueIds
|
||||
}
|
||||
|
||||
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
|
||||
if (options?.limit && options?.limit > 0) {
|
||||
return Math.min(options.limit, props.length)
|
||||
}
|
||||
|
||||
return props.length
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await buildModel({ client, _class, keys: config, options })}
|
||||
{#if !isLoading}
|
||||
<Loading />
|
||||
{/if}
|
||||
{:then attributeModels}
|
||||
<div class="listRoot">
|
||||
{#if docObjects}
|
||||
{#each docObjects as docObject, rowIndex (docObject._id)}
|
||||
<div
|
||||
class="listGrid"
|
||||
class:mListGridChecked={selectedIssueIds.has(docObject._id)}
|
||||
class:mListGridFixed={rowIndex === selectedRowIndex}
|
||||
>
|
||||
{#each attributeModels as attributeModel, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<div class="antiTable-cells__checkCell">
|
||||
<CheckBox
|
||||
checked={selectedIssueIds.has(docObject._id)}
|
||||
on:change={(event) => {
|
||||
handleIssueSelected(docObject._id, event)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="issuePresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
<div
|
||||
id="context-menu"
|
||||
class="eIssuePresenterContextMenu"
|
||||
on:click={(event) => showMenu(event, docObject, rowIndex)}
|
||||
>
|
||||
<IconMoreV size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="gridElement">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{:else if loadingProps !== undefined}
|
||||
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
|
||||
<div class="listGrid mListGridIsLoading" class:fixed={rowIndex === selectedRowIndex}>
|
||||
{#each attributeModels as _, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<CheckBox checked={false} />
|
||||
<div class="ml-4">
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
{#if isLoading}
|
||||
<Loading />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.listRoot {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.listGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 9rem auto 4rem 2rem;
|
||||
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);
|
||||
}
|
||||
|
||||
&.mListGridFixed {
|
||||
.eIssuePresenterContextMenu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.mListGridIsLoading {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.gridElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
|
||||
.eIssuePresenterContextMenu {
|
||||
visibility: hidden;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.eIssuePresenterContextMenu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!--
|
||||
// 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 { Tooltip } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: number
|
||||
|
||||
$: shortModificationDate = new Date(value).toLocaleString('default', { month: 'short', day: 'numeric' })
|
||||
$: fullModificationDate = new Date(value).toLocaleString('default', {
|
||||
minute: '2-digit',
|
||||
hour: 'numeric',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})
|
||||
</script>
|
||||
|
||||
<Tooltip label={tracker.string.ModificationDate} props={{ value: fullModificationDate }}>
|
||||
<span class="nowrap">{shortModificationDate}</span>
|
||||
</Tooltip>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
// 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 { Issue } from '@anticrm/tracker'
|
||||
import { Icon } from '@anticrm/ui'
|
||||
import { issueStatuses } from '../../utils'
|
||||
|
||||
export let value: Issue
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="titlePresenter">
|
||||
<div class="icon">
|
||||
<Icon icon={issueStatuses[value.status].icon} size={'small'} />
|
||||
</div>
|
||||
<span class="label nowrap" title={value.title}>{value.title}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.titlePresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user