Tracker: Issues list view (#1313)

Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
Artyom Grigorovich 2022-04-09 01:05:49 +07:00 committed by GitHub
parent 207bcf73bb
commit de0a5fcdd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 534 additions and 120 deletions

19
.vscode/settings.json vendored
View File

@ -1,19 +0,0 @@
{
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"**/.rush": true,
"**/lib": true,
"**/.heft": true,
"**/_api-extractor-temp": true,
"**/svelte.config.js": true,
"**/postcss.config.js": true,
"**/node_modules": true,
"**/temp": true
},
"explorerExclude.backup": null
}

View File

@ -0,0 +1,56 @@
<!--
// 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 { formatName, Person } from '@anticrm/contact'
import { Hierarchy } from '@anticrm/core'
import { Avatar } from '@anticrm/presentation'
import { showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
export let value: Person | undefined
export let inline: boolean = false
export let shouldShowName = true
export let shouldShowPlaceholder = false
const avatarSize = 'x-small'
const onClick = async () => {
if (value) {
showPanel(view.component.EditDoc, value._id, Hierarchy.mixinOrClass(value), 'full')
}
}
</script>
{#if value || shouldShowPlaceholder}
{#if value}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{encodeURIComponent([view.component.EditDoc, value._id, Hierarchy.mixinOrClass(value)].join('|'))}"
on:click={onClick}
>
<div class="icon">
<Avatar size={avatarSize} avatar={value?.avatar} />
</div>
{#if shouldShowName}
<span class="label">{formatName(value.name)}</span>
{/if}
</a>
{:else}
<div class="icon">
<Avatar size={avatarSize} avatar={undefined} />
</div>
{/if}
{/if}

View File

@ -15,33 +15,26 @@
--> -->
<script lang="ts"> <script lang="ts">
import { formatName, Person } from '@anticrm/contact' import { formatName, Person } from '@anticrm/contact'
import { Hierarchy } from '@anticrm/core' import { IntlString } from '@anticrm/platform'
import { Avatar } from '@anticrm/presentation' import { Tooltip } from '@anticrm/ui'
import { showPanel } from '@anticrm/ui' import PersonContent from './PersonContent.svelte'
import view from '@anticrm/view'
export let value: Person export let value: Person
export let inline: boolean = false export let inline: boolean = false
export let showLabel = true export let shouldShowName = true
export let shouldShowPlaceholder = false
async function onClick () { export let tooltipLabels: { personLabel: IntlString; placeholderLabel?: IntlString } | undefined = undefined
showPanel(view.component.EditDoc, value._id, Hierarchy.mixinOrClass(value), 'full')
}
</script> </script>
{#if value} {#if value || shouldShowPlaceholder}
{#if tooltipLabels}
<a <Tooltip
class="flex-presenter" label={value ? tooltipLabels.personLabel : tooltipLabels.placeholderLabel}
class:inline-presenter={inline} props={{ value: formatName(value?.name) }}
href="#{encodeURIComponent([view.component.EditDoc, value._id, Hierarchy.mixinOrClass(value)].join('|'))}" >
on:click={onClick} <PersonContent {inline} {value} {shouldShowName} {shouldShowPlaceholder} />
> </Tooltip>
<div class="icon"> {:else}
<Avatar size={'x-small'} avatar={value.avatar} /> <PersonContent {inline} {value} {shouldShowName} {shouldShowPlaceholder} />
</div> {/if}
{#if showLabel}
<span class="label">{formatName(value.name)}</span>
{/if}
</a>
{/if} {/if}

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Doc, Ref } from '@anticrm/core' import { Doc, Ref, getObjectValue } from '@anticrm/core'
import { IconMoreV, showPopup } from '@anticrm/ui' import { IconMoreV, showPopup } from '@anticrm/ui'
import { AttributeModel } from '@anticrm/view' import { AttributeModel } from '@anticrm/view'
import inventory, { Category } from '@anticrm/inventory' import inventory, { Category } from '@anticrm/inventory'
@ -29,19 +29,6 @@
export let parent: Ref<Doc> = inventory.global.Category export let parent: Ref<Doc> = inventory.global.Category
let expanded: Set<Ref<Category>> = new Set<Ref<Category>>() let expanded: Set<Ref<Category>> = new Set<Ref<Category>>()
function getValue (doc: Category, key: string): any {
if (key.length === 0) {
return doc
}
const path = key.split('.')
const len = path.length
let obj = doc as any
for (let i = 0; i < len; i++) {
obj = obj?.[path[i]]
}
return obj ?? ''
}
const showMenu = async (ev: MouseEvent, object: Category): Promise<void> => { const showMenu = async (ev: MouseEvent, object: Category): Promise<void> => {
showPopup(ContextMenu, { object }, ev.target as HTMLElement) showPopup(ContextMenu, { object }, ev.target as HTMLElement)
} }
@ -74,13 +61,21 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<svelte:component this={attribute.presenter} value={getValue(object, attribute.key)} {...attribute.props} /> <svelte:component
this={attribute.presenter}
value={getObjectValue(attribute.key, object) ?? ''}
{...attribute.props}
/>
<div class="menuRow" on:click={(ev) => showMenu(ev, object)}><IconMoreV size={'small'} /></div> <div class="menuRow" on:click={(ev) => showMenu(ev, object)}><IconMoreV size={'small'} /></div>
</div> </div>
</td> </td>
{:else} {:else}
<td> <td>
<svelte:component this={attribute.presenter} value={getValue(object, attribute.key)} {...attribute.props} /> <svelte:component
this={attribute.presenter}
value={getObjectValue(attribute.key, object)}
{...attribute.props}
/>
</td> </td>
{/if} {/if}
{/each} {/each}

View File

@ -11,10 +11,14 @@
"Issues": "Issues", "Issues": "Issues",
"Views": "Views", "Views": "Views",
"Active": "Active", "Active": "Active",
"AllIssues": "All issues {value}",
"ActiveIssues": "Active issues {value}",
"BacklogIssues": "Backlog {value}",
"Backlog": "Backlog", "Backlog": "Backlog",
"Board": "Board", "Board": "Board",
"Projects": "Projects", "Projects": "Projects",
"CreateTeam": "Create team", "CreateTeam": "Create team",
"AddIssue": "Add Issue",
"NewIssue": "New issue", "NewIssue": "New issue",
"SaveIssue": "Save issue", "SaveIssue": "Save issue",
"CreateMore": "Create more", "CreateMore": "Create more",
@ -35,6 +39,8 @@
"Status": "", "Status": "",
"Number": "Number", "Number": "Number",
"Assignee": "Assignee", "Assignee": "Assignee",
"AssignTo": "Assign to",
"AssignedTo": "Assigned to {value}",
"Parent": "Set parent issue\u2026", "Parent": "Set parent issue\u2026",
"BlockedBy": "", "BlockedBy": "",
"RelatedTo": "", "RelatedTo": "",
@ -44,6 +50,7 @@
"Project": "Project", "Project": "Project",
"Space": "", "Space": "",
"DueDate": "Set due date\u2026", "DueDate": "Set due date\u2026",
"ModificationDate": "Last Modified: {value}",
"Team": "", "Team": "",
"Issue": "", "Issue": "",
"Document": "", "Document": "",
@ -55,4 +62,4 @@
"AddIssueTooltip": "Add issue..." "AddIssueTooltip": "Add issue..."
}, },
"status": {} "status": {}
} }

View File

@ -1,12 +1,10 @@
<script lang='ts'> <script lang="ts">
import { Ref } from '@anticrm/core' import { Ref } from '@anticrm/core'
import { IssueStatus, Team } from '@anticrm/tracker' import { IssueStatus, Team } from '@anticrm/tracker'
import Issues from './Issues.svelte' import Issues from './Issues.svelte'
export let currentSpace: Ref<Team> import tracker from '../../plugin'
let todalIssues = 0 export let currentSpace: Ref<Team>
</script> </script>
<div class='fs-title'>
Active issues {todalIssues} <Issues {currentSpace} categories={[IssueStatus.InProgress, IssueStatus.Todo]} title={tracker.string.ActiveIssues} />
</div>
<Issues currentSpace={currentSpace} categories={[IssueStatus.InProgress, IssueStatus.Todo]} on:content={(evt) => { todalIssues = evt.detail.length } }></Issues>

View File

@ -1,7 +1,10 @@
<script lang='ts'> <script lang="ts">
import { Ref } from '@anticrm/core' import { Ref } from '@anticrm/core'
import { IssueStatus, Team } from '@anticrm/tracker' import { IssueStatus, Team } from '@anticrm/tracker'
import Issues from './Issues.svelte' import Issues from './Issues.svelte'
import tracker from '../../plugin'
export let currentSpace: Ref<Team> export let currentSpace: Ref<Team>
</script> </script>
<Issues {currentSpace} categories={[IssueStatus.Backlog]}></Issues>
<Issues title={tracker.string.BacklogIssues} {currentSpace} categories={[IssueStatus.Backlog]} />

View File

@ -105,7 +105,7 @@
<div class='flex-between mb-2'> <div class='flex-between mb-2'>
<IssuePresenter value={object} {currentTeam}/> <IssuePresenter value={object} {currentTeam}/>
{#if issue.$lookup?.assignee } {#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} {/if}
</div> </div>
<span class='fs-bold title'> <span class='fs-bold title'>

View File

@ -1,52 +1,97 @@
<script lang="ts"> <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 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 query: DocumentQuery<Issue>
export let category: IssueStatus export let category: IssueStatus
export let currentSpace: Ref<Team> | undefined = undefined
export let currentTeam: Team export let currentTeam: Team
const dispatch = createEventDispatcher()
const options: FindOptions<Issue> = { const options: FindOptions<Issue> = {
lookup: { lookup: {
assignee: contact.class.Employee 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> </script>
<div class='category' class:visible={visible}> <div class="category" class:visible={issuesAmount > 0}>
<div class='fs-title'> <div class="header categoryHeader flex-between label">
{IssueStatus[category]} <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> </div>
<Scroller> <Scroller>
<Table <IssuesList
_class={tracker.class.Issue} _class={tracker.class.Issue}
config={[ config={[
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam }, label: tracker.string.Issue }, { key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
'title', { key: '', presenter: tracker.component.TitlePresenter },
// 'status', { key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
'$lookup.assignee', {
'modifiedOn' key: '$lookup.assignee',
]} props: {
options={options} shouldShowName: false,
query={{ ...query, status: category }} shouldShowPlaceholder: true,
showNotification tooltipLabels: { personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }
highlightRows }
on:content={(evt) => { visible = evt.detail.length > 0 }} }
/> ]}
</Scroller> {options}
query={{ ...query, status: category }}
on:content={(evt) => {
issuesAmount = evt.detail.length
dispatch('content', issuesAmount)
}}
/>
</Scroller>
</div> </div>
<style lang="scss"> <style lang="scss">
.category { .category {
display: none; display: none;
&.visible { &.visible {
display: block; 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> </style>

View File

@ -18,6 +18,7 @@
import type { Issue, Team } from '@anticrm/tracker' import type { Issue, Team } from '@anticrm/tracker'
import { Icon, showPanel } from '@anticrm/ui' import { Icon, showPanel } from '@anticrm/ui'
import tracker from '../../plugin' import tracker from '../../plugin'
import { issuePriorities } from '../../utils'
export let value: Issue export let value: Issue
export let currentTeam: Team export let currentTeam: Team
@ -37,6 +38,9 @@
class:inline-presenter={inline} class:inline-presenter={inline}
on:click={show} on:click={show}
> >
<div class="icon">
<Icon icon={issuePriorities[value.priority].icon} size={'small'} />
</div>
<div class="icon"> <div class="icon">
<Icon icon={tracker.icon.Issue} size={'small'} /> <Icon icon={tracker.icon.Issue} size={'small'} />
</div> </div>

View File

@ -16,9 +16,10 @@
import type { DocumentQuery, Ref } from '@anticrm/core' import type { DocumentQuery, Ref } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation' import { createQuery } from '@anticrm/presentation'
import { Issue, IssueStatus, Team } from '@anticrm/tracker' import { Issue, IssueStatus, Team } from '@anticrm/tracker'
import { ScrollBox } from '@anticrm/ui' import { Label, ScrollBox } from '@anticrm/ui'
import CategoryPresenter from './CategoryPresenter.svelte' import CategoryPresenter from './CategoryPresenter.svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import { IntlString } from '@anticrm/platform'
export let currentSpace: Ref<Team> export let currentSpace: Ref<Team>
export let categories = [ export let categories = [
@ -28,15 +29,26 @@
IssueStatus.Done, IssueStatus.Done,
IssueStatus.Canceled IssueStatus.Canceled
] ]
export let title: IntlString = tracker.string.AllIssues
export let query: DocumentQuery<Issue> = {} export let query: DocumentQuery<Issue> = {}
export let search: string = '' 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 = $: resultQuery =
search === '' ? { space: currentSpace, ...query } : { $search: search, space: currentSpace, ...query } search === '' ? { space: currentSpace, ...query } : { $search: search, space: currentSpace, ...query }
const spaceQuery = createQuery()
let currentTeam: Team | undefined let currentTeam: Team | undefined
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => { $: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
@ -46,9 +58,21 @@
{#if currentTeam} {#if currentTeam}
<ScrollBox vertical stretch> <ScrollBox vertical stretch>
<div class="fs-title">
<Label label={title} params={{ value: getTotalIssues() }} />
</div>
<div class="ml-4 mt-4"> <div class="ml-4 mt-4">
{#each categories as category} {#each categories as category}
<CategoryPresenter {category} query={resultQuery} {currentTeam} /> <CategoryPresenter
{category}
query={resultQuery}
{currentSpace}
{currentTeam}
on:content={(event) => {
issuesMap[category] = event.detail
}}
/>
{/each} {/each}
</div> </div>
</ScrollBox> </ScrollBox>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -26,6 +26,8 @@ import MyIssues from './components/myissues/MyIssues.svelte'
import Projects from './components/projects/Projects.svelte' import Projects from './components/projects/Projects.svelte'
import Views from './components/views/Views.svelte' import Views from './components/views/Views.svelte'
import IssuePresenter from './components/issues/IssuePresenter.svelte' import IssuePresenter from './components/issues/IssuePresenter.svelte'
import TitlePresenter from './components/issues/TitlePresenter.svelte'
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
import EditIssue from './components/issues/EditIssue.svelte' import EditIssue from './components/issues/EditIssue.svelte'
import NewIssueHeader from './components/NewIssueHeader.svelte' import NewIssueHeader from './components/NewIssueHeader.svelte'
@ -41,6 +43,8 @@ export default async (): Promise<Resources> => ({
Projects, Projects,
Views, Views,
IssuePresenter, IssuePresenter,
TitlePresenter,
ModificationDatePresenter,
EditIssue, EditIssue,
NewIssueHeader NewIssueHeader
} }

View File

@ -29,11 +29,15 @@ export default mergeIds(trackerId, tracker, {
Issues: '' as IntlString, Issues: '' as IntlString,
Views: '' as IntlString, Views: '' as IntlString,
Active: '' as IntlString, Active: '' as IntlString,
AllIssues: '' as IntlString,
ActiveIssues: '' as IntlString,
BacklogIssues: '' as IntlString,
Backlog: '' as IntlString, Backlog: '' as IntlString,
Board: '' as IntlString, Board: '' as IntlString,
Project: '' as IntlString, Project: '' as IntlString,
Projects: '' as IntlString, Projects: '' as IntlString,
CreateTeam: '' as IntlString, CreateTeam: '' as IntlString,
AddIssue: '' as IntlString,
NewIssue: '' as IntlString, NewIssue: '' as IntlString,
Team: '' as IntlString, Team: '' as IntlString,
SelectTeam: '' as IntlString, SelectTeam: '' as IntlString,
@ -56,6 +60,8 @@ export default mergeIds(trackerId, tracker, {
Status: '' as IntlString, Status: '' as IntlString,
Number: '' as IntlString, Number: '' as IntlString,
Assignee: '' as IntlString, Assignee: '' as IntlString,
AssignTo: '' as IntlString,
AssignedTo: '' as IntlString,
Parent: '' as IntlString, Parent: '' as IntlString,
BlockedBy: '' as IntlString, BlockedBy: '' as IntlString,
RelatedTo: '' as IntlString, RelatedTo: '' as IntlString,
@ -64,6 +70,7 @@ export default mergeIds(trackerId, tracker, {
Labels: '' as IntlString, Labels: '' as IntlString,
Space: '' as IntlString, Space: '' as IntlString,
DueDate: '' as IntlString, DueDate: '' as IntlString,
ModificationDate: '' as IntlString,
Issue: '' as IntlString, Issue: '' as IntlString,
Document: '' as IntlString, Document: '' as IntlString,
DocumentIcon: '' as IntlString, DocumentIcon: '' as IntlString,
@ -86,6 +93,8 @@ export default mergeIds(trackerId, tracker, {
Board: '' as AnyComponent, Board: '' as AnyComponent,
Projects: '' as AnyComponent, Projects: '' as AnyComponent,
IssuePresenter: '' as AnyComponent, IssuePresenter: '' as AnyComponent,
TitlePresenter: '' as AnyComponent,
ModificationDatePresenter: '' as AnyComponent,
EditIssue: '' as AnyComponent, EditIssue: '' as AnyComponent,
CreateTeam: '' as AnyComponent, CreateTeam: '' as AnyComponent,
NewIssueHeader: '' as AnyComponent NewIssueHeader: '' as AnyComponent

View File

@ -15,7 +15,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core' import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
import { SortingOrder } from '@anticrm/core' import { SortingOrder, getObjectValue } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation' import { createQuery, getClient } from '@anticrm/presentation'
import { Component, CheckBox, IconDown, IconUp, Label, Loading, showPopup, Spinner } from '@anticrm/ui' import { Component, CheckBox, IconDown, IconUp, Label, Loading, showPopup, Spinner } from '@anticrm/ui'
import { BuildModelKey } from '@anticrm/view' import { BuildModelKey } from '@anticrm/view'
@ -80,19 +80,6 @@
} }
$: update(_class, query, sortKey, sortOrder, options) $: update(_class, query, sortKey, sortOrder, options)
function getValue (doc: Doc, key: string): any {
if (key.length === 0) {
return doc
}
const path = key.split('.')
const len = path.length
let obj = doc as any
for (let i = 0; i < len; i++) {
obj = obj?.[path[i]]
}
return obj ?? ''
}
const client = getClient() const client = getClient()
const showMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => { const showMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
@ -219,7 +206,7 @@
<div class="antiTable-cells__firstCell"> <div class="antiTable-cells__firstCell">
<svelte:component <svelte:component
this={attribute.presenter} this={attribute.presenter}
value={getValue(object, attribute.key)} value={getObjectValue(attribute.key, object) ?? ''}
{...attribute.props} {...attribute.props}
/> />
<div id='context-menu' class="antiTable-cells__firstCell-menuRow" on:click={(ev) => showMenu(ev, object, row)}> <div id='context-menu' class="antiTable-cells__firstCell-menuRow" on:click={(ev) => showMenu(ev, object, row)}>
@ -231,7 +218,7 @@
<td> <td>
<svelte:component <svelte:component
this={attribute.presenter} this={attribute.presenter}
value={getValue(object, attribute.key)} value={getObjectValue(attribute.key, object) ?? ''}
{...attribute.props} {...attribute.props}
/> />
</td> </td>

View File

@ -27,6 +27,7 @@ import IntlStringPresenter from './components/IntlStringPresenter.svelte'
import NumberEditor from './components/NumberEditor.svelte' import NumberEditor from './components/NumberEditor.svelte'
import NumberPresenter from './components/NumberPresenter.svelte' import NumberPresenter from './components/NumberPresenter.svelte'
import Table from './components/Table.svelte' import Table from './components/Table.svelte'
import Menu from './components/Menu.svelte'
import TableView from './components/TableView.svelte' import TableView from './components/TableView.svelte'
import TimestampPresenter from './components/TimestampPresenter.svelte' import TimestampPresenter from './components/TimestampPresenter.svelte'
import { deleteObject } from './utils' import { deleteObject } from './utils'
@ -40,7 +41,7 @@ import view from './plugin'
export { default as ContextMenu } from './components/Menu.svelte' export { default as ContextMenu } from './components/Menu.svelte'
export { buildModel, getActions, getObjectPresenter, LoadingProps, getCollectionCounter } from './utils' export { buildModel, getActions, getObjectPresenter, LoadingProps, getCollectionCounter } from './utils'
export { Table, TableView, EditDoc, ColorsPopup } export { Table, TableView, EditDoc, ColorsPopup, Menu }
function Delete (object: Doc): void { function Delete (object: Doc): void {
showPopup( showPopup(

View File

@ -71,6 +71,7 @@ export async function getObjectPresenter (
_class, _class,
label: preserveKey.label ?? clazz.label, label: preserveKey.label ?? clazz.label,
presenter, presenter,
props: preserveKey.props,
sortingKey sortingKey
} }
} }