TSK-955: Fix status display (#2840)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-03-27 22:33:23 +07:00 committed by GitHub
parent 948f39d669
commit 57a925b038
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 145 additions and 61 deletions

View File

@ -40,5 +40,8 @@ export const calcRank = (prev?: { rank: string }, next?: { rank: string }): stri
const a = prev?.rank !== undefined ? LexoRank.parse(prev.rank) : LexoRank.min()
const b = next?.rank !== undefined ? LexoRank.parse(next.rank) : LexoRank.max()
if (a.equals(b)) {
return a.genNext().toString()
}
return a.between(b).toString()
}

View File

@ -104,6 +104,10 @@ export function getClient (): TxOperations {
* @public
*/
export function setClient (_client: Client): void {
if (liveQuery !== undefined) {
void liveQuery.close()
}
const needRefresh = liveQuery !== undefined
liveQuery = new LQ(_client)
client = new UIClient(_client, liveQuery)
_client.notify = (tx: Tx) => {
@ -111,17 +115,23 @@ export function setClient (_client: Client): void {
txListeners.forEach((it) => it(tx))
}
if (needRefresh) {
refreshClient()
}
}
/**
* @public
*/
export function refreshClient (): void {
if (liveQuery !== undefined) {
void liveQuery.refreshConnect()
void liveQuery?.refreshConnect()
for (const q of globalQueries) {
q.refreshClient()
}
}
const globalQueries: LiveQuery[] = []
/**
* @public
*/
@ -137,6 +147,8 @@ export class LiveQuery {
onDestroy(() => {
this.unsubscribe()
})
} else {
globalQueries.push(this)
}
}
@ -149,11 +161,21 @@ export class LiveQuery {
if (!this.needUpdate(_class, query, callback, options)) {
return false
}
return this.doQuery<T>(_class, query, callback, options)
}
private doQuery<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
callback: (result: FindResult<T>) => void,
options: FindOptions<T> | undefined
): boolean {
this.unsubscribe()
this.oldCallback = callback
this.oldClass = _class
this.oldOptions = options
this.oldQuery = query
const unsub = liveQuery.query(_class, query, callback, options)
this.unsubscribe = () => {
unsub()
@ -166,6 +188,16 @@ export class LiveQuery {
return true
}
refreshClient (): void {
if (this.oldClass !== undefined && this.oldQuery !== undefined && this.oldCallback !== undefined) {
const _class = this.oldClass
const query = this.oldQuery
const callback = this.oldCallback
const options = this.oldOptions
this.doQuery(_class, query, callback, options)
}
}
private needUpdate<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,

View File

@ -64,7 +64,7 @@ interface Query {
* @public
*/
export class LiveQuery extends TxProcessor implements Client {
private readonly client: Client
private client: Client
private readonly queries: Map<Ref<Class<Doc>>, Query[]> = new Map<Ref<Class<Doc>>, Query[]>()
private readonly queue: Query[] = []
@ -73,6 +73,11 @@ export class LiveQuery extends TxProcessor implements Client {
this.client = client
}
async updateClient (client: Client): Promise<void> {
this.client = client
await this.refreshConnect()
}
async close (): Promise<void> {
return await this.client.close()
}
@ -89,7 +94,20 @@ export class LiveQuery extends TxProcessor implements Client {
async refreshConnect (): Promise<void> {
for (const q of [...this.queue]) {
if (!(await this.removeFromQueue(q))) {
await this.refresh(q)
try {
await this.refresh(q)
} catch (err) {
console.error(err)
}
}
}
for (const v of this.queries.values()) {
for (const q of v) {
try {
await this.refresh(q)
} catch (err) {
console.error(err)
}
}
}
}

View File

@ -50,7 +50,7 @@ class RequestPromise {
resolve!: (value?: any) => void
reject!: (reason?: any) => void
reconnect?: () => void
constructor () {
constructor (readonly method: string, readonly params: any[]) {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
@ -64,6 +64,7 @@ class Connection implements ClientConnection {
private lastId = 0
private readonly interval: number
private readonly sessionId = generateId() as string
private closed = false
constructor (
private readonly url: string,
@ -80,6 +81,7 @@ class Connection implements ClientConnection {
}
async close (): Promise<void> {
this.closed = true
clearInterval(this.interval)
if (this.websocket !== null) {
if (this.websocket instanceof Promise) {
@ -158,7 +160,7 @@ class Connection implements ClientConnection {
}
this.requests.delete(resp.id)
if (resp.error !== undefined) {
console.log('ERROR', resp.id)
console.log('ERROR', promise, resp.id)
promise.reject(new PlatformError(resp.error))
} else {
promise.resolve(resp.result)
@ -212,8 +214,11 @@ class Connection implements ClientConnection {
// If not defined, on reconnect with timeout, will retry automatically.
retry?: () => Promise<boolean>
}): Promise<any> {
if (this.closed) {
throw new PlatformError(unknownError('connection closed'))
}
const id = this.lastId++
const promise = new RequestPromise()
const promise = new RequestPromise(data.method, data.params)
const sendData = async (): Promise<void> => {
if (this.websocket instanceof Promise) {

View File

@ -34,10 +34,8 @@ import { FilterQuery } from '@hcengineering/view-resources'
import { get, writable } from 'svelte/store'
import contact from './plugin'
const client = getClient()
export async function getChannelProviders (): Promise<Map<Ref<ChannelProvider>, ChannelProvider>> {
const cp = await client.findAll(contact.class.ChannelProvider, {})
const cp = await getClient().findAll(contact.class.ChannelProvider, {})
const map = new Map<Ref<ChannelProvider>, ChannelProvider>()
for (const provider of cp) {
map.set(provider._id, provider)

View File

@ -39,6 +39,8 @@ export const genRanks = (count: number): Generator<string, void, unknown> =>
export const calcRank = (prev?: { rank: string }, next?: { rank: string }): string => {
const a = prev?.rank !== undefined ? LexoRank.parse(prev.rank) : LexoRank.min()
const b = next?.rank !== undefined ? LexoRank.parse(next.rank) : LexoRank.max()
if (a.equals(b)) {
return a.genNext().toString()
}
return a.between(b).toString()
}

View File

@ -15,6 +15,7 @@
<svg
class="svg-{size}"
{fill}
id={category._id}
style:transform={category._id === tracker.issueStatusCategory.Started ? 'rotate(-90deg)' : ''}
viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"

View File

@ -4,7 +4,7 @@
import { Issue, IssueStatus } from '@hcengineering/tracker'
import { Label, ticker } from '@hcengineering/ui'
import tracker from '../../plugin'
import { statusByIdStore } from '../../utils'
import { statusStore } from '../../utils'
import Duration from './Duration.svelte'
import StatusPresenter from './StatusPresenter.svelte'
@ -83,7 +83,7 @@
displaySt = result
}
$: updateStatus(txes, $statusByIdStore, $ticker)
$: updateStatus(txes, $statusStore.byId, $ticker)
</script>
<div class="flex-row mt-4 mb-4">

View File

@ -40,7 +40,7 @@
$: if (value.category === tracker.issueStatusCategory.Started) {
const _s = [
...$statusStore.filter(
...$statusStore.statuses.filter(
(it) => it.attachedTo === value.attachedTo && it.category === tracker.issueStatusCategory.Started
)
]

View File

@ -65,7 +65,7 @@
)
}
$: statuses = $statusStore.filter((it) => it.attachedTo === value?.space)
$: statuses = $statusStore.statuses.filter((it) => it.attachedTo === value?.space)
$: selectedStatus = statuses?.find((status) => status._id === value.status) ?? statuses?.[0]
$: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { IssueStatus } from '@hcengineering/tracker'
import { statusByIdStore } from '../../utils'
import { statusStore } from '../../utils'
import IssueStatusIcon from './IssueStatusIcon.svelte'
export let value: IssueStatus | undefined
@ -22,7 +22,7 @@
</script>
{#if value}
{@const icon = $statusByIdStore.get(value._id)?.$lookup?.category?.icon}
{@const icon = $statusStore.byId.get(value._id)?.$lookup?.category?.icon}
<div class="flex-presenter">
{#if icon}
<IssueStatusIcon {value} {size} />

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IssueStatus } from '@hcengineering/tracker'
import { statusByIdStore } from '../../utils'
import { statusStore } from '../../utils'
import StatusPresenter from './StatusPresenter.svelte'
export let value: Ref<IssueStatus> | undefined
@ -23,5 +23,5 @@
</script>
{#if value}
<StatusPresenter value={$statusByIdStore.get(value)} {size} />
<StatusPresenter value={$statusStore.byId.get(value)} {size} />
{/if}

View File

@ -26,16 +26,16 @@
</script>
{#if value}
<DocNavLink object={value} {onClick} component={tracker.component.EditIssue} inline shrink={1}>
<span
class="name overflow-label select-text"
class:with-margin={shouldUseMargin}
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
title={value.title}
>
<span
class="name overflow-label select-text"
class:with-margin={shouldUseMargin}
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
title={value.title}
>
<DocNavLink object={value} {onClick} component={tracker.component.EditIssue} inline shrink={1}>
{value.title}
</span>
</DocNavLink>
</DocNavLink>
</span>
{#if showParent}
<ParentNamesPresenter {value} />
{/if}

View File

@ -29,7 +29,7 @@
} from '@hcengineering/ui'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import { statusByIdStore, statusStore, subIssueListProvider } from '../../../utils'
import { statusStore, subIssueListProvider } from '../../../utils'
export let value: WithLookup<Issue>
export let currentProject: Project | undefined = undefined
@ -77,7 +77,7 @@
}
$: if (subIssues) {
const doneStatuses = $statusStore
const doneStatuses = $statusStore.statuses
.filter((s) => s.category === tracker.issueStatusCategory.Completed)
.map((p) => p._id)
countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
@ -115,7 +115,7 @@
id: iss._id,
text,
isSelected: iss._id === value._id,
...getIssueStatusIcon(iss, $statusByIdStore)
...getIssueStatusIcon(iss, $statusStore.byId)
}
}),
width: 'large'

View File

@ -29,7 +29,7 @@
} from '@hcengineering/ui'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import { statusByIdStore, statusStore, subIssueListProvider } from '../../../utils'
import { statusStore, subIssueListProvider } from '../../../utils'
export let object: WithLookup<Doc & { related: number }> | undefined
export let value: WithLookup<Doc & { related: number }> | undefined
@ -69,7 +69,7 @@
}
$: if (subIssues) {
const doneStatuses = $statusStore
const doneStatuses = $statusStore.statuses
.filter((s) => s.category === tracker.issueStatusCategory.Completed)
.map((p) => p._id)
countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
@ -101,7 +101,7 @@
value: subIssues.map((iss) => {
const text = currentProject ? `${getIssueId(currentProject, iss)} ${iss.title}` : iss.title
return { id: iss._id, text, isSelected: false, ...getIssueStatusIcon(iss, $statusByIdStore) }
return { id: iss._id, text, isSelected: false, ...getIssueStatusIcon(iss, $statusStore.byId) }
}),
width: 'large'
},

View File

@ -17,7 +17,7 @@
import { Issue } from '@hcengineering/tracker'
import { floorFractionDigits, Label } from '@hcengineering/ui'
import tracker from '../../plugin'
import { statusByIdStore } from '../../utils'
import { statusStore } from '../../utils'
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
export let docs: Issue[] | undefined = undefined
@ -28,13 +28,13 @@
$: noParents = docs?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
$: rootNoBacklogIssues = noParents?.filter(
(it) => $statusByIdStore.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
(it) => $statusStore.byId.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
)
$: totalEstimation = floorFractionDigits(
(rootNoBacklogIssues ?? [{ estimation: 0, childInfo: [] } as unknown as Issue])
.map((it) => {
const cat = $statusByIdStore.get(it.status)?.category
const cat = $statusStore.byId.get(it.status)?.category
let retEst = it.estimation
if (it.childInfo?.length > 0) {

View File

@ -66,10 +66,10 @@
}
async function addStatus () {
if (editingStatus?.name && editingStatus?.category && $statusStore) {
const categoryStatuses = $statusStore.filter((s) => s.category === editingStatus!.category)
if (editingStatus?.name && editingStatus?.category) {
const categoryStatuses = $statusStore.statuses.filter((s) => s.category === editingStatus!.category)
const prevStatus = categoryStatuses[categoryStatuses.length - 1]
const nextStatus = $statusStore[$statusStore.findIndex(({ _id }) => _id === prevStatus._id) + 1]
const nextStatus = $statusStore.statuses[$statusStore.statuses.findIndex(({ _id }) => _id === prevStatus._id) + 1]
isSaving = true
await client.addCollection(
@ -93,9 +93,9 @@
}
async function editStatus () {
if ($statusStore && statusCategories && editingStatus?.name && editingStatus?.category && '_id' in editingStatus) {
if (statusCategories && editingStatus?.name && editingStatus?.category && '_id' in editingStatus) {
const statusId = '_id' in editingStatus ? editingStatus._id : undefined
const status = statusId && $statusStore.find(({ _id }) => _id === statusId)
const status = statusId && $statusStore.byId.get(statusId)
if (!status) {
return
@ -157,12 +157,14 @@
},
undefined,
async (result) => {
if (result && project && $statusStore) {
if (result && project) {
isSaving = true
await client.removeDoc(status._class, status.space, status._id)
if (project.defaultIssueStatus === status._id) {
const newDefaultStatus = $statusStore.find((s) => s._id !== status._id && s.category === status.category)
const newDefaultStatus = $statusStore.statuses.find(
(s) => s._id !== status._id && s.category === status.category && s.space === status.space
)
if (newDefaultStatus?._id) {
await updateProjectDefaultStatus(newDefaultStatus._id)
}
@ -196,12 +198,12 @@
}
async function handleDrop (toItem: IssueStatus) {
if ($statusStore && draggingStatus?._id !== toItem._id && draggingStatus?.category === toItem.category) {
if (draggingStatus?._id !== toItem._id && draggingStatus?.category === toItem.category) {
const fromIndex = getStatusIndex(draggingStatus)
const toIndex = getStatusIndex(toItem)
const [prev, next] = [
$statusStore[fromIndex < toIndex ? toIndex : toIndex - 1],
$statusStore[fromIndex < toIndex ? toIndex + 1 : toIndex]
$statusStore.statuses[fromIndex < toIndex ? toIndex : toIndex - 1],
$statusStore.statuses[fromIndex < toIndex ? toIndex + 1 : toIndex]
]
isSaving = true
@ -213,7 +215,7 @@
}
function getStatusIndex (status: IssueStatus) {
return $statusStore?.findIndex(({ _id }) => _id === status._id) ?? -1
return $statusStore.statuses.findIndex(({ _id }) => _id === status._id) ?? -1
}
function resetDrag () {
@ -242,14 +244,14 @@
</div>
</svelte:fragment>
{#if project === undefined || statusCategories === undefined || $statusStore === undefined}
{#if project === undefined || statusCategories === undefined || $statusStore.statuses.length === 0}
<Loading />
{:else}
<Scroller>
<div class="popupPanel-body__main-content py-10 clear-mins">
{#each statusCategories as category}
{@const statuses =
$statusStore?.filter((s) => s.attachedTo === projectId && s.category === category._id) ?? []}
$statusStore.statuses.filter((s) => s.attachedTo === projectId && s.category === category._id) ?? []}
{@const isSingle = statuses.length === 1}
<div class="flex-between category-name">
<Label label={category.label} />

View File

@ -677,16 +677,27 @@ export async function removeProject (project: Project): Promise<void> {
await client.removeDoc(tracker.class.Project, core.space.Space, project._id)
}
/**
* @public
*/
export interface StatusStore {
statuses: Array<WithLookup<IssueStatus>>
byId: IdMap<WithLookup<IssueStatus>>
version: number
}
// Issue status live query
export const statusByIdStore = writable<IdMap<WithLookup<IssueStatus>>>(new Map())
export const statusStore = writable<Array<WithLookup<IssueStatus>>>([])
export const statusStore = writable<StatusStore>({ statuses: [], byId: new Map(), version: 0 })
const query = createQuery(true)
query.query(
tracker.class.IssueStatus,
{},
(res) => {
statusStore.set(res)
statusByIdStore.set(toIdMap(res))
statusStore.update((old) => ({
version: old.version + 1,
statuses: res,
byId: toIdMap(res)
}))
},
{
lookup: {

View File

@ -39,6 +39,8 @@ export const genRanks = (count: number): Generator<string, void, unknown> =>
export const calcRank = (prev?: { rank: string }, next?: { rank: string }): string => {
const a = prev?.rank !== undefined ? LexoRank.parse(prev.rank) : LexoRank.min()
const b = next?.rank !== undefined ? LexoRank.parse(next.rank) : LexoRank.max()
if (a.equals(b)) {
return a.genNext().toString()
}
return a.between(b).toString()
}

View File

@ -39,6 +39,8 @@ export async function connect (title: string): Promise<Client | undefined> {
}
_token = token
let clientSet = false
const clientFactory = await getResource(client.function.GetClient)
_client = await clientFactory(
token,
@ -54,7 +56,15 @@ export async function connect (title: string): Promise<Client | undefined> {
})
},
// We need to refresh all active live queries and clear old queries.
refreshClient
() => {
try {
if (clientSet) {
refreshClient()
}
} catch (err) {
console.error(err)
}
}
)
console.log('logging in as', email)
@ -72,6 +82,7 @@ export async function connect (title: string): Promise<Client | undefined> {
// Update on connect, so it will be triggered
setClient(_client)
clientSet = true
return
}
@ -105,6 +116,7 @@ export async function connect (title: string): Promise<Client | undefined> {
return _client
}
function clearMetadata (ws: string): void {
const tokens = fetchMetadataLocalStorage(login.metadata.LoginTokens)
if (tokens !== null) {

View File

@ -16,8 +16,8 @@
"dev-uitest": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://localhost:3333 SETTING=storage-dev.json playwright test --browser chromium --reporter list,html -c ./tests/playwright.config.ts",
"debug": "playwright test --browser chromium -c ./tests/playwright.config.ts --debug --headed",
"dev-debug": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://localhost:3333 SETTING=storage-dev.json playwright test --browser chromium -c ./tests/playwright.config.ts --debug --headed",
"codegen": "playwright codegen --load-storage storage.json http://localhost:8083/workbench",
"dev-codegen": "cross-env playwright codegen --load-storage storage-dev.json http://localhost:8080/workbench"
"codegen": "playwright codegen --load-storage storage.json http://localhost:8083/workbench/sanity-ws/",
"dev-codegen": "cross-env playwright codegen --load-storage storage-dev.json http://localhost:8080/workbench/sanity-ws/"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",

View File

@ -205,9 +205,7 @@ test('create-issue-draft', async ({ page }) => {
// Click text=Issues >> nth=1
await page.locator('text=Issues').nth(1).click()
await expect(page).toHaveURL(
'http://localhost:8083/workbench/sanity-ws/tracker/tracker%3Aproject%3ADefaultProject/issues'
)
await expect(page).toHaveURL(/.*\/workbench\/sanity-ws\/tracker\/tracker%3Aproject%3ADefaultProject\/issues/)
await expect(page.locator('#new-issue')).toHaveText('New issue')
// Click button:has-text("New issue")
await page.locator('#new-issue').click()