Various fixes (#2099)

This commit is contained in:
Andrey Sobolev 2022-06-18 15:11:10 +07:00 committed by GitHub
parent c412044ea6
commit 8441f0b5ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 291 additions and 22 deletions

View File

@ -2,6 +2,17 @@
## 0.6.28 (upcoming)
Tracker:
- Issue state history.
- Subissue issue popup.
Lead:
- Lead presentation changed to number.
- Title column for leads.
- Fix New Lead action for Organization.
## 0.6.27
Platform:

View File

@ -149,6 +149,7 @@ export function createModel (builder: Builder): void {
descriptor: task.viewlet.StatusTable,
config: [
'',
'title',
'$lookup.attachedTo',
'$lookup.state',
'$lookup.doneState',
@ -253,7 +254,7 @@ export function createModel (builder: Builder): void {
icon: lead.icon.Lead,
input: 'focus',
category: lead.category.Lead,
target: contact.class.Person,
target: contact.class.Contact,
context: { mode: ['context', 'browser'] },
override: [lead.action.CreateGlobalLead]
})

View File

@ -313,6 +313,7 @@
<slot name="beforeCard" {state} />
<KanbanRow
bind:this={stateRows[si]}
on:obj-focus
{stateObjects}
{isDragging}
{dragCard}

View File

@ -6,7 +6,7 @@
"Save": "Сохранить",
"Minutes": "{minutes, plural, =0 {меньше минуты назад} =1 {минуту назад} other {# минут назад}}",
"Hours": "{hours, plural, =0 {меньше часа назад} =1 {час назад} other {# часов назад}}",
"Days": "{days, plural, =0 {сегода} =1 {вчера} other {# дней назад}}",
"Days": "{days, plural, =0 {сегодня} =1 {вчера} other {# дней назад}}",
"Months": "{months, plural, =0 {в этом месяце} =1 {месяц назад} =2 {2 месяца назад} =3 {3 месяца назад} =4 {4 месяца назад} other {# месяцев назад}}",
"Years": "{years, plural, =0 {в этом году} =1 {год назад} =2 {2 года назад} =3 {3 года назад} =4 {4 года назад} other {# лет назад}}",
"ShowMore": "Показать больше",

View File

@ -52,6 +52,8 @@
on:click={() => {
dispatch('close', item.id)
}}
on:focus={() => dispatch('update', item)}
on:mouseover={() => dispatch('update', item)}
>
{#if hasSelected}
<div class="icon">

View File

@ -32,6 +32,6 @@
<div class="icon">
<Icon icon={lead.icon.Lead} size={'small'} />
</div>
<span class="label">{value.title}</span>
<span class="label nowrap">LEAD-{value.number}</span>
</a>
{/if}

View File

@ -141,7 +141,14 @@
"AnyFilter": "any filter",
"AllFilters": "all filters",
"NoDescription": "No description",
"SearchIssue": "Search for task..."
"SearchIssue": "Search for task...",
"DurMinutes": "{minutes, plural, =0 {less than a minute} =1 {a minute} other {# minutes}}",
"DurHours": "{hours, plural, =0 {less than an hour} =1 {an hour} other {# hours}}",
"DurDays": "{days, plural, =0 {today} =1 {1 day} other {# days }}",
"DurMonths": "{months, plural, =0 {this month} =1 {1 month} other {# months}}",
"DurYears": "{years, plural, =0 {this year} =1 {a year} other {# years}}",
"StatusHistory": "State History"
},
"status": {}
}

View File

@ -141,7 +141,14 @@
"AnyFilter": "любому фильтру",
"AllFilters": "всем фильтрам",
"NoDescription": "Нет описания",
"SearchIssue": "Поиск задачи..."
"SearchIssue": "Поиск задачи...",
"DurMinutes": "{minutes, plural, =0 {меньше минуты} =1 {1 минута} =2 {2 минуты} =3 {3 минуты} =4 {4 минуты} other {# минут}}",
"DurHours": "{hours, plural, =0 {меньше часа} =1 {1 час} =2 {2 часа} =3 {3 часа} =4 {часа} =21 {21 час} =22 {22 часа} =23 {23 часа} =24 {24 часа} other {# часов}}",
"DurDays": "{days, plural, =0 {сегодня} =1 {1 день} =2 {2 дня} =3 {3 дня} =4 {4 дня} other {# дней }}",
"DurMonths": "{months, plural, =0 {меньше месяця} =1 {месяц} =2 {2 месяца} =3 {3 месяца} =4 {4 месяца} other {# месяцев}}",
"DurYears": "{years, plural, =0 {меньше года} =1 {год} =2 {2 года} =3 {3 года} =4 {4 года} other {# лет}}",
"StatusHistory": "История состояний"
},
"status": {}
}

View File

@ -0,0 +1,60 @@
<!--
// 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" context="module">
const SECOND = 1000
const MINUTE = SECOND * 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
const MONTH = DAY * 30
const YEAR = MONTH * 12
</script>
<script lang="ts">
import { translate } from '@anticrm/platform'
import tracker from '../../plugin'
export let value: number
let time: string = ''
async function formatTime (passed: number) {
if (passed < 0) passed = 0
if (passed < HOUR) {
time = await translate(tracker.string.DurMinutes, { minutes: Math.floor(passed / MINUTE) })
} else if (passed < DAY) {
time = await translate(tracker.string.DurHours, { hours: Math.floor(passed / HOUR) })
} else if (passed < MONTH) {
time = await translate(tracker.string.DurDays, { days: Math.floor(passed / DAY) })
} else if (passed < YEAR) {
time = await translate(tracker.string.DurMonths, { months: Math.floor(passed / MONTH) })
} else {
time = await translate(tracker.string.DurYears, { years: Math.floor(passed / YEAR) })
}
}
$: tooltipValue = new Date(value).toLocaleString('default', {
minute: '2-digit',
hour: 'numeric',
day: '2-digit',
month: 'short',
year: 'numeric'
})
$: formatTime(value)
</script>
<span style="white-space: nowrap;">
{time}
</span>

View File

@ -21,6 +21,7 @@
import AssigneeEditor from './AssigneeEditor.svelte'
import PriorityEditor from './PriorityEditor.svelte'
import StatusEditor from './StatusEditor.svelte'
import IssueStatusActivity from './IssueStatusActivity.svelte'
export let object: Issue
let issue: Issue | undefined
@ -31,7 +32,7 @@
const spaceQuery = createQuery()
const statusesQuery = createQuery()
issueQuery.query(
$: issueQuery.query(
object._class,
{ _id: object._id },
(res) => {
@ -87,6 +88,11 @@
<AssigneeEditor value={issue} tooltipFill={false} />
{/if}
</div>
<IssueStatusActivity {issue} />
<div class="mb-2">
<Label label={tracker.string.Description} />:
</div>
{#if issue.description}
<div class="descr ml-2" class:mask={cHeight >= limit} bind:clientHeight={cHeight}>
<MessageViewer message={issue.description} />

View File

@ -0,0 +1,126 @@
<script lang="ts">
import core, { Ref, Timestamp, Tx, TxCollectionCUD, TxCreateDoc, TxUpdateDoc, WithLookup } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { Issue, IssueStatus } from '@anticrm/tracker'
import { Icon, Label, ticker } from '@anticrm/ui'
import tracker from '../../plugin'
import Duration from './Duration.svelte'
import StatusPresenter from './StatusPresenter.svelte'
export let issue: Issue
const query = createQuery()
let txes: Tx[] = []
interface WithTime {
status: WithLookup<IssueStatus>
duration: number
}
const stQuery = createQuery()
let statuses = new Map<Ref<IssueStatus>, WithLookup<IssueStatus>>()
stQuery.query(
tracker.class.IssueStatus,
{},
(res) => {
statuses = new Map(res.map((it) => [it._id, it]))
},
{
lookup: {
category: tracker.class.IssueStatusCategory
}
}
)
$: query.query(
core.class.Tx,
{ 'tx.objectId': issue._id },
(res) => {
txes = res
},
{ sort: { modifiedOn: 1 } }
)
let displaySt: WithTime[] = []
async function updateStatus (
txes: Tx[],
statuses: Map<Ref<IssueStatus>, WithLookup<IssueStatus>>,
now: number
): Promise<void> {
const result: WithTime[] = []
let current: Ref<IssueStatus> | undefined
let last: Timestamp = Date.now()
for (let it of txes) {
if (it._class === core.class.TxCollectionCUD) {
it = (it as TxCollectionCUD<Issue, Issue>).tx
}
let newStatus: Ref<IssueStatus> | undefined
if (it._class === core.class.TxCreateDoc) {
const op = it as TxCreateDoc<Issue>
if (op.attributes.status !== undefined) {
newStatus = op.attributes.status
last = it.modifiedOn
}
}
if (it._class === core.class.TxUpdateDoc) {
const op = it as TxUpdateDoc<Issue>
if (op.operations.status !== undefined) {
newStatus = op.operations.status
}
}
if (current === undefined) {
current = newStatus
last = it.modifiedOn
} else if (current !== newStatus && newStatus !== undefined) {
let stateValue = result.find((it) => it.status?._id === current)
if (stateValue === undefined) {
stateValue = { status: statuses.get(current) as IssueStatus, duration: 0 }
result.push(stateValue)
}
stateValue.duration += it.modifiedOn - last
current = newStatus
last = it.modifiedOn
}
}
if (current !== undefined) {
let stateValue = result.find((it) => it.status?._id === current)
if (stateValue === undefined) {
stateValue = { status: statuses.get(current) as IssueStatus, duration: 0 }
result.push(stateValue)
}
stateValue.duration += Date.now() - last
}
result.sort((a, b) => b.duration - a.duration)
displaySt = result
}
$: updateStatus(txes, statuses, $ticker)
</script>
<div class="flex-row mt-4 mb-4">
<Label label={tracker.string.StatusHistory} />:
<table class="ml-2">
{#each displaySt as st}
<tr>
<td class="flex-row-center mt-2 mb-2">
{#if st?.status?.$lookup?.category?.icon !== undefined}
<div class="mr-2">
<Icon icon={st.status.$lookup?.category.icon} size={'small'} />
</div>
{/if}
<StatusPresenter value={st.status} />
</td>
<td>
<div class="ml-2 mr-2">
<Duration value={st.duration} />
</div>
</td>
</tr>
{/each}
</table>
</div>

View File

@ -13,10 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Data, Doc, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import { AttachmentDocList } from '@anticrm/attachment-resources'
import { Class, Data, Doc, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import notification from '@anticrm/notification'
import { Panel } from '@anticrm/panel'
import { getResource } from '@anticrm/platform'
import presentation, { createQuery, getClient, MessageViewer } from '@anticrm/presentation'
import { StyledTextArea } from '@anticrm/text-editor'
import type { Issue, IssueStatus, Team } from '@anticrm/tracker'
import {
Button,
@ -31,16 +34,14 @@
Spinner
} from '@anticrm/ui'
import { ContextMenu } from '@anticrm/view-resources'
import { StyledTextArea } from '@anticrm/text-editor'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import tracker from '../../../plugin'
import { getIssueId } from '../../../utils'
import IssueStatusActivity from '../IssueStatusActivity.svelte'
import ControlPanel from './ControlPanel.svelte'
import CopyToClipboard from './CopyToClipboard.svelte'
import SubIssueSelector from './SubIssueSelector.svelte'
import SubIssues from './SubIssues.svelte'
import { getResource } from '@anticrm/platform'
import notification from '@anticrm/notification'
import SubIssueSelector from './SubIssueSelector.svelte'
export let _id: Ref<Issue>
export let _class: Ref<Class<Issue>>
@ -275,6 +276,9 @@
{#if issue && currentTeam && issueStatuses}
<ControlPanel {issue} {issueStatuses} />
{/if}
<div class="divider" />
<IssueStatusActivity {issue} />
</svelte:fragment>
</Panel>
{/if}
@ -298,4 +302,11 @@
color: var(--theme-content-trans-color);
}
}
.divider {
margin-top: 1rem;
margin-bottom: 1rem;
grid-column: 1 / 3;
height: 1px;
background-color: var(--divider-color);
}
</style>

View File

@ -15,9 +15,9 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import { WithLookup } from '@anticrm/core'
import { Doc, WithLookup } from '@anticrm/core'
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
import { ContextMenu } from '@anticrm/view-resources'
import { ActionContext, ContextMenu, ListSelectionProvider, SelectDirection } from '@anticrm/view-resources'
import { showPanel, showPopup } from '@anticrm/ui'
import tracker from '../../../plugin'
import { getIssueId } from '../../../utils'
@ -71,8 +71,21 @@
{ getBoundingClientRect: () => DOMRect.fromRect({ width: 1, height: 1, x: ev.clientX, y: ev.clientY }) }
)
}
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
// if (dir === 'vertical') {
// // Select next
// table.select(offset, of)
// }
})
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
{#each issues as issue, index (issue._id)}
<div
class="flex-between row"
@ -88,6 +101,12 @@
on:dragenter={() => (hoveringIndex = index)}
on:drop|preventDefault={(ev) => handleDrop(ev, index)}
on:dragend={resetDrag}
on:mouseover={() => {
listProvider.updateFocus(issue)
}}
on:focus={() => {
listProvider.updateFocus(issue)
}}
>
<div class="draggable-container">
<div class="draggable-mark"><Circles /></div>

View File

@ -17,6 +17,7 @@
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import { Button, closeTooltip, ProgressCircle, SelectPopup, showPanel, showPopup } from '@anticrm/ui'
import { updateFocus } from '@anticrm/view-resources'
import tracker from '../../../plugin'
import { getIssueId } from '../../../utils'
@ -54,7 +55,6 @@
showPanel(tracker.component.EditIssue, target, issue._class, 'content')
}
}
function showSubIssues () {
if (subIssues) {
closeTooltip()
@ -77,7 +77,16 @@
return DOMRect.fromRect({ width: 1, height: 1, x: rect.left + offsetX, y: rect.bottom + offsetY })
}
},
(selectedIssue) => selectedIssue !== undefined && openIssue(selectedIssue)
(selectedIssue) => {
selectedIssue !== undefined && openIssue(selectedIssue)
},
(selectedIssue) => {
const focus = subIssues?.find((it) => it._id === selectedIssue.id)
if (focus !== undefined) {
console.log('ISE', selectedIssue, focus)
updateFocus({ focus })
}
}
)
}
}

View File

@ -157,7 +157,14 @@ export default mergeIds(trackerId, tracker, {
IncludeItemsThatMatch: '' as IntlString,
AnyFilter: '' as IntlString,
AllFilters: '' as IntlString,
NoDescription: '' as IntlString
NoDescription: '' as IntlString,
DurMinutes: '' as IntlString,
DurHours: '' as IntlString,
DurDays: '' as IntlString,
DurMonths: '' as IntlString,
DurYears: '' as IntlString,
StatusHistory: '' as IntlString
},
component: {
NopeComponent: '' as AnyComponent,

View File

@ -66,12 +66,10 @@
}
$: ctx = $contextStore[$contextStore.length - 1]
$: mode = $contextStore[$contextStore.length - 1]?.mode
$: application = $contextStore[$contextStore.length - 1]?.application
$: if (ctx !== undefined) {
updateActions(
{ mode: ctx.mode as ViewContextType, application: ctx.application },
$focusStore.focus,
$selectionStore
)
updateActions({ mode: mode as ViewContextType, application: application }, $focusStore.focus, $selectionStore)
}
function keyPrefix (key: KeyboardEvent): string {
return (

View File

@ -1,4 +1,5 @@
import { Doc } from '@anticrm/core'
import { panelstore } from '@anticrm/ui'
import { onDestroy } from 'svelte'
import { writable } from 'svelte/store'
@ -53,13 +54,16 @@ export const selectionStore = writable<Doc[]>([])
export const previewDocument = writable<Doc | undefined>()
panelstore.subscribe((val) => {
previewDocument.set(undefined)
})
/**
* @public
*/
export function updateFocus (selection?: FocusSelection): void {
focusStore.update((cur) => {
const now = Date.now()
if (selection === undefined || now - ((cur as any).now ?? 0) >= 25) {
if (selection === undefined || now - ((cur as any).now ?? 0) >= 50) {
cur.focus = selection?.focus
cur.provider = selection?.provider
;(cur as any).now = now