mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-26 13:01:48 +03:00
Various fixes (#2099)
This commit is contained in:
parent
c412044ea6
commit
8441f0b5ab
11
changelog.md
11
changelog.md
@ -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:
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -313,6 +313,7 @@
|
||||
<slot name="beforeCard" {state} />
|
||||
<KanbanRow
|
||||
bind:this={stateRows[si]}
|
||||
on:obj-focus
|
||||
{stateObjects}
|
||||
{isDragging}
|
||||
{dragCard}
|
||||
|
@ -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": "Показать больше",
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
|
@ -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": {}
|
||||
}
|
||||
|
@ -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": {}
|
||||
}
|
||||
|
@ -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>
|
@ -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} />
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user