UBERF-6330: Fix race conditions in UI (#5184)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-04-16 14:53:53 +07:00 committed by GitHub
parent d3fa4908ef
commit 0d1d1a8b8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 862 additions and 595 deletions

View File

@ -27,12 +27,21 @@ export class MeasureMetricsContext implements MeasureContext {
this.logger.logOperation(this.name, spend, { ...params, ...fullParams }) this.logger.logOperation(this.name, spend, { ...params, ...fullParams })
}) })
const errorPrinter = ({ message, stack, ...rest }: Error): object => ({
message,
stack,
...rest
})
function replacer (value: any): any {
return value instanceof Error ? errorPrinter(value) : value
}
this.logger = logger ?? { this.logger = logger ?? {
info: (msg, args) => { info: (msg, args) => {
console.info(msg, ...Object.entries(args ?? {}).map((it) => `${it[0]}=${JSON.stringify(it[1])}`)) console.info(msg, ...Object.entries(args ?? {}).map((it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`))
}, },
error: (msg, args) => { error: (msg, args) => {
console.error(msg, ...Object.entries(args ?? {}).map((it) => `${it[0]}=${JSON.stringify(it[1])}`)) console.error(msg, ...Object.entries(args ?? {}).map((it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`))
}, },
close: async () => {}, close: async () => {},
logOperation: (operation, time, params) => {} logOperation: (operation, time, params) => {}

View File

@ -60,6 +60,15 @@ export interface ModelLogger {
error: (msg: string, err: any) => void error: (msg: string, err: any) => void
} }
const errorPrinter = ({ message, stack, ...rest }: Error): object => ({
message,
stack,
...rest
})
function replacer (value: any): any {
return value instanceof Error ? errorPrinter(value) : value
}
/** /**
* @public * @public
*/ */
@ -68,6 +77,6 @@ export const consoleModelLogger: ModelLogger = {
console.log(msg, data) console.log(msg, data)
}, },
error (msg: string, data: any): void { error (msg: string, data: any): void {
console.error(msg, data) console.error(msg, replacer(data))
} }
} }

View File

@ -116,7 +116,7 @@
if (key.code === 'Enter') { if (key.code === 'Enter') {
key.preventDefault() key.preventDefault()
key.stopPropagation() key.stopPropagation()
handleSelection(key, objects, selection) void handleSelection(key, objects, selection)
} }
} }
const manager = createFocusManager() const manager = createFocusManager()
@ -217,7 +217,7 @@
class="menu-item withList w-full flex-row-center" class="menu-item withList w-full flex-row-center"
disabled={readonly || isDeselectDisabled || loading} disabled={readonly || isDeselectDisabled || loading}
on:click={() => { on:click={() => {
handleSelection(undefined, objects, item) void handleSelection(undefined, objects, item)
}} }}
> >
<span class="label" class:disabled={readonly || isDeselectDisabled || loading}> <span class="label" class:disabled={readonly || isDeselectDisabled || loading}>

View File

@ -33,7 +33,7 @@
import presentation from '../plugin' import presentation from '../plugin'
import { ObjectSearchCategory, ObjectSearchResult } from '../types' import { ObjectSearchCategory, ObjectSearchResult } from '../types'
import { getClient } from '../utils' import { getClient } from '../utils'
import { hasResource } from '..' import { hasResource, reduceCalls } from '..'
export let query: string = '' export let query: string = ''
export let label: IntlString | undefined = undefined export let label: IntlString | undefined = undefined
@ -108,7 +108,7 @@
export function done () {} export function done () {}
async function updateItems ( const updateItems = reduceCalls(async function updateItems (
cat: ObjectSearchCategory | undefined, cat: ObjectSearchCategory | undefined,
query: string, query: string,
relatedDocuments?: RelatedDocument[] relatedDocuments?: RelatedDocument[]
@ -148,8 +148,8 @@
} }
} }
} }
} })
$: updateItems(category, query, relatedDocuments) $: void updateItems(category, query, relatedDocuments)
const manager = createFocusManager() const manager = createFocusManager()

View File

@ -39,7 +39,7 @@
import view, { IconProps } from '@hcengineering/view' import view, { IconProps } from '@hcengineering/view'
import { ObjectCreate } from '../types' import { ObjectCreate } from '../types'
import { getClient } from '../utils' import { getClient, reduceCalls } from '../utils'
import SpacesPopup from './SpacesPopup.svelte' import SpacesPopup from './SpacesPopup.svelte'
export let _class: Ref<Class<Space>> export let _class: Ref<Class<Space>>
@ -73,19 +73,24 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const mgr = getFocusManager() const mgr = getFocusManager()
async function updateSelected (_value: Ref<Space> | undefined, spaceQuery: DocumentQuery<Space> | undefined) { const updateSelected = reduceCalls(async function (
selected = _value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: _value }) : undefined _value: Ref<Space> | undefined,
spaceQuery: DocumentQuery<Space> | undefined
) {
let v = _value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: _value }) : undefined
selected = v
if (selected === undefined && autoSelect) { if (selected === undefined && autoSelect) {
selected = (await findDefaultSpace?.()) ?? (await client.findOne(_class, { ...(spaceQuery ?? {}) })) v = (await findDefaultSpace?.()) ?? (await client.findOne(_class, { ...(spaceQuery ?? {}) }))
selected = v
if (selected !== undefined) { if (selected !== undefined) {
value = selected._id ?? undefined value = selected._id ?? undefined
} }
} }
dispatch('object', selected) dispatch('object', selected)
} })
$: updateSelected(value, spaceQuery) $: void updateSelected(value, spaceQuery)
const showSpacesPopup = (ev: MouseEvent) => { const showSpacesPopup = (ev: MouseEvent) => {
if (readonly) { if (readonly) {

View File

@ -14,6 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import { Analytics } from '@hcengineering/analytics'
import core, { import core, {
TxOperations, TxOperations,
getCurrentAccount, getCurrentAccount,
@ -51,7 +52,6 @@ import { onDestroy } from 'svelte'
import { type KeyedAttribute } from '..' import { type KeyedAttribute } from '..'
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline' import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
import plugin from './plugin' import plugin from './plugin'
import { Analytics } from '@hcengineering/analytics'
let liveQuery: LQ let liveQuery: LQ
let client: TxOperations & MeasureClient let client: TxOperations & MeasureClient
@ -552,3 +552,42 @@ export function decodeTokenPayload (token: string): any {
export function isAdminUser (): boolean { export function isAdminUser (): boolean {
return decodeTokenPayload(getMetadata(plugin.metadata.Token) ?? '').admin === 'true' return decodeTokenPayload(getMetadata(plugin.metadata.Token) ?? '').admin === 'true'
} }
type ReduceParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
interface NextCall {
op: () => Promise<void>
}
/**
* Utility method to skip middle update calls, optimistically if update function is called multiple times with few different parameters, only the last variant will be executed.
* The last invocation is executed after a few cycles, allowing to skip middle ones.
*
* This method can be used inside Svelte components to collapse complex update logic and handle interactions.
*/
export function reduceCalls<T extends (...args: ReduceParameters<T>) => Promise<void>> (
operation: T
): (...args: ReduceParameters<T>) => Promise<void> {
let nextCall: NextCall | undefined
let currentCall: NextCall | undefined
const next = (): void => {
currentCall = nextCall
nextCall = undefined
if (currentCall !== undefined) {
void currentCall.op()
}
}
return async function (...args: ReduceParameters<T>): Promise<void> {
const myOp = async (): Promise<void> => {
await operation(...args)
next()
}
nextCall = { op: myOp }
await Promise.resolve()
if (currentCall === undefined) {
next()
}
}
}

View File

@ -14,10 +14,10 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { SearchResultDoc } from '@hcengineering/core'
import presentation, { SearchResult, reduceCalls, searchFor, type SearchItem } from '@hcengineering/presentation'
import { Label, ListView, resizeObserver } from '@hcengineering/ui' import { Label, ListView, resizeObserver } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import presentation, { type SearchItem, SearchResult, searchFor } from '@hcengineering/presentation'
import { SearchResultDoc } from '@hcengineering/core'
export let query: string = '' export let query: string = ''
@ -67,12 +67,12 @@
return false return false
} }
async function updateItems (localQuery: string): Promise<void> { const updateItems = reduceCalls(async function (localQuery: string): Promise<void> {
const r = await searchFor('mention', localQuery) const r = await searchFor('mention', localQuery)
if (r.query === query) { if (r.query === query) {
items = r.items items = r.items
} }
} })
$: void updateItems(query) $: void updateItems(query)
</script> </script>

View File

@ -43,5 +43,6 @@
close={popup.close} close={popup.close}
{contentPanel} {contentPanel}
overlay={popup.options.overlay} overlay={popup.options.overlay}
{popup}
/> />
{/each} {/each}

View File

@ -16,11 +16,11 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { deviceOptionsStore as deviceInfo, resizeObserver, testing } from '..' import { deviceOptionsStore as deviceInfo, resizeObserver, testing } from '..'
import { fitPopupElement } from '../popups' import { CompAndProps, fitPopupElement } from '../popups'
import type { AnySvelteComponent, PopupAlignment, PopupOptions, PopupPositionElement, DeviceOptions } from '../types' import type { AnySvelteComponent, DeviceOptions, PopupAlignment, PopupOptions, PopupPositionElement } from '../types'
export let is: AnySvelteComponent export let is: AnySvelteComponent
export let props: object export let props: Record<string, any>
export let element: PopupAlignment | undefined export let element: PopupAlignment | undefined
export let onClose: ((result: any) => void) | undefined export let onClose: ((result: any) => void) | undefined
export let onUpdate: ((result: any) => void) | undefined export let onUpdate: ((result: any) => void) | undefined
@ -29,11 +29,16 @@
export let top: boolean export let top: boolean
export let close: () => void export let close: () => void
export let contentPanel: HTMLElement | undefined export let contentPanel: HTMLElement | undefined
export let popup: CompAndProps
// We should not update props after popup is created, // We should not update props after popup is created using standard mechanism,
// since they could be used, and any show will update them // since they could be used, and any show will update them
const initialProps = props // So special update callback should be used.
let initialProps: Record<string, any> = props
$: popup.update = (props) => {
initialProps = Object.assign(initialProps, props)
}
const WINDOW_PADDING = 1 const WINDOW_PADDING = 1
interface PopupParams { interface PopupParams {
@ -81,7 +86,7 @@
close() close()
} }
function escapeClose () { function escapeClose (): void {
if (componentInstance?.canClose) { if (componentInstance?.canClose) {
if (!componentInstance.canClose()) return if (!componentInstance.canClose()) return
} }
@ -109,6 +114,8 @@
function handleKeydown (ev: KeyboardEvent) { function handleKeydown (ev: KeyboardEvent) {
if (ev.key === 'Escape' && is && top) { if (ev.key === 'Escape' && is && top) {
ev.preventDefault()
ev.stopPropagation()
escapeClose() escapeClose()
} }
} }
@ -268,6 +275,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
id={popup.options.refId}
class="popup {testing ? 'endShow' : showing === undefined ? 'endShow' : !showing ? 'preShow' : 'startShow'}" class="popup {testing ? 'endShow' : showing === undefined ? 'endShow' : !showing ? 'preShow' : 'startShow'}"
class:testing class:testing
class:anim={(element === 'float' || element === 'centered') && !testing && !drag} class:anim={(element === 'float' || element === 'centered') && !testing && !drag}

View File

@ -22,16 +22,19 @@ export interface CompAndProps {
onClose?: (result: any) => void onClose?: (result: any) => void
onUpdate?: (result: any) => void onUpdate?: (result: any) => void
close: () => void close: () => void
update?: (props: Record<string, any>) => void
options: { options: {
category: string category: string
overlay: boolean overlay: boolean
fixed?: boolean fixed?: boolean
refId?: string
} }
} }
export interface PopupResult { export interface PopupResult {
id: string id: string
close: () => void close: () => void
update: (props: Record<string, any>) => void
} }
export const popupstore = writable<CompAndProps[]>([]) export const popupstore = writable<CompAndProps[]>([])
@ -40,7 +43,7 @@ export function updatePopup (id: string, props: Partial<CompAndProps>): void {
popupstore.update((popups) => { popupstore.update((popups) => {
const popupIndex = popups.findIndex((p) => p.id === id) const popupIndex = popups.findIndex((p) => p.id === id)
if (popupIndex !== -1) { if (popupIndex !== -1) {
popups[popupIndex] = { ...popups[popupIndex], ...props } popups[popupIndex].update?.(props)
} }
return popups return popups
}) })
@ -63,7 +66,11 @@ export function showPopup (
category: string category: string
overlay: boolean overlay: boolean
fixed?: boolean fixed?: boolean
} = { category: 'popup', overlay: true } refId?: string
} = {
category: 'popup',
overlay: true
}
): PopupResult { ): PopupResult {
const id = `${popupId++}` const id = `${popupId++}`
const closePopupOp = (): void => { const closePopupOp = (): void => {
@ -90,7 +97,10 @@ export function showPopup (
} }
return { return {
id, id,
close: closePopupOp close: closePopupOp,
update: (props) => {
updatePopup(id, props)
}
} }
} }

View File

@ -109,7 +109,7 @@
} }
} }
$: updateFilterActions( $: void updateFilterActions(
messages, messages,
filters, filters,
selectedFiltersRefs, selectedFiltersRefs,

View File

@ -124,12 +124,12 @@
const q = createQuery() const q = createQuery()
async function update ( function update (
_class: Ref<Class<Event>>, _class: Ref<Class<Event>>,
query: DocumentQuery<Event> | undefined, query: DocumentQuery<Event> | undefined,
calendars: Calendar[], calendars: Calendar[],
options?: FindOptions<Event> options?: FindOptions<Event>
) { ): void {
q.query<Event>( q.query<Event>(
_class, _class,
query ?? { space: { $in: calendars.map((p) => p._id) } }, query ?? { space: { $in: calendars.map((p) => p._id) } },

View File

@ -43,7 +43,7 @@
$: updateResultQuery(search) $: updateResultQuery(search)
function showCreateDialog () { function showCreateDialog (): void {
if (createComponent === undefined) { if (createComponent === undefined) {
return return
} }

View File

@ -50,7 +50,7 @@
_class: Ref<Class<ActivityMessage>>, _class: Ref<Class<ActivityMessage>>,
lastViewedTimestamp?: Timestamp, lastViewedTimestamp?: Timestamp,
selectedMessageId?: Ref<ActivityMessage> selectedMessageId?: Ref<ActivityMessage>
) { ): void {
if (dataProvider === undefined) { if (dataProvider === undefined) {
// For now loading all messages for documents with activity. Need to correct handle aggregation with pagination. // For now loading all messages for documents with activity. Need to correct handle aggregation with pagination.
// Perhaps we should load all activity messages once, and keep loading in chunks only for ChatMessages then merge them correctly with activity messages // Perhaps we should load all activity messages once, and keep loading in chunks only for ChatMessages then merge them correctly with activity messages

View File

@ -41,13 +41,13 @@
let title: string | undefined = undefined let title: string | undefined = undefined
let description: string | undefined = undefined let description: string | undefined = undefined
$: updateDescription(_id, _class, object) $: void updateDescription(_id, _class, object)
$: getChannelName(_id, _class, object).then((res) => { $: void getChannelName(_id, _class, object).then((res) => {
title = res title = res
}) })
async function updateDescription (_id: Ref<Doc>, _class: Ref<Class<Doc>>, object?: Doc) { async function updateDescription (_id: Ref<Doc>, _class: Ref<Class<Doc>>, object?: Doc): Promise<void> {
if (hierarchy.isDerived(_class, chunter.class.DirectMessage)) { if (hierarchy.isDerived(_class, chunter.class.DirectMessage)) {
description = undefined description = undefined
} else if (hierarchy.isDerived(_class, chunter.class.Channel)) { } else if (hierarchy.isDerived(_class, chunter.class.Channel)) {

View File

@ -30,7 +30,7 @@
$: updatePersons(ids) $: updatePersons(ids)
function updatePersons (ids: Ref<Person>[]) { function updatePersons (ids: Ref<Person>[]): void {
persons = ids.map((_id) => $personByIdStore.get(_id)).filter((person): person is Person => !!person) persons = ids.map((_id) => $personByIdStore.get(_id)).filter((person): person is Person => !!person)
} }
</script> </script>

View File

@ -13,11 +13,10 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Person } from '@hcengineering/contact'
import { personByIdStore, Avatar } from '@hcengineering/contact-resources'
import { Doc, IdMap, Ref, WithLookup } from '@hcengineering/core'
import { getLocation, Label, navigate, TimeSince } from '@hcengineering/ui'
import activity, { ActivityMessage } from '@hcengineering/activity' import activity, { ActivityMessage } from '@hcengineering/activity'
import { Person } from '@hcengineering/contact'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { Doc, IdMap, Ref, WithLookup } from '@hcengineering/core'
import notification, { import notification, {
ActivityInboxNotification, ActivityInboxNotification,
DocNotifyContext, DocNotifyContext,
@ -25,6 +24,7 @@
InboxNotificationsClient InboxNotificationsClient
} from '@hcengineering/notification' } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { Label, TimeSince, getLocation, navigate } from '@hcengineering/ui'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { buildThreadLink } from '../utils' import { buildThreadLink } from '../utils'
@ -40,7 +40,7 @@
let inboxClient: InboxNotificationsClient | undefined = undefined let inboxClient: InboxNotificationsClient | undefined = undefined
getResource(notification.function.GetInboxNotificationsClient).then((getClientFn) => { void getResource(notification.function.GetInboxNotificationsClient).then((getClientFn) => {
inboxClient = getClientFn() inboxClient = getClientFn()
}) })

View File

@ -39,7 +39,7 @@
$: disabledRemoveFor = currentAccount._id !== object?.createdBy && creatorPersonRef ? [creatorPersonRef] : [] $: disabledRemoveFor = currentAccount._id !== object?.createdBy && creatorPersonRef ? [creatorPersonRef] : []
$: updateMembers(object) $: updateMembers(object)
function updateMembers (object: Channel | undefined) { function updateMembers (object: Channel | undefined): void {
if (object === undefined) { if (object === undefined) {
members = new Set() members = new Set()
return return
@ -58,7 +58,7 @@
) )
} }
async function changeMembers (personRefs: Ref<Person>[], object?: Channel) { async function changeMembers (personRefs: Ref<Person>[], object?: Channel): Promise<void> {
if (object === undefined) { if (object === undefined) {
return return
} }
@ -75,7 +75,7 @@
await Promise.all([leaveChannel(object, toLeave), joinChannel(object, toJoin)]) await Promise.all([leaveChannel(object, toLeave), joinChannel(object, toJoin)])
} }
async function removeMember (ev: CustomEvent) { async function removeMember (ev: CustomEvent): Promise<void> {
if (object === undefined) { if (object === undefined) {
return return
} }
@ -97,7 +97,7 @@
await leaveChannel(object, accounts) await leaveChannel(object, accounts)
} }
function openSelectUsersPopup () { function openSelectUsersPopup (): void {
showPopup( showPopup(
SelectUsersPopup, SelectUsersPopup,
{ {
@ -108,7 +108,7 @@
'top', 'top',
(result?: Ref<Person>[]) => { (result?: Ref<Person>[]) => {
if (result != null) { if (result != null) {
changeMembers(result, object) void changeMembers(result, object)
} }
} }
) )

View File

@ -16,7 +16,7 @@
import contact, { Contact, Employee, Person, getName } from '@hcengineering/contact' import contact, { Contact, Employee, Person, getName } from '@hcengineering/contact'
import { Class, DocumentQuery, FindOptions, Ref } from '@hcengineering/core' import { Class, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform' import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation' import presentation, { getClient, reduceCalls } from '@hcengineering/presentation'
import { import {
ActionIcon, ActionIcon,
Button, Button,
@ -76,13 +76,13 @@
const client = getClient() const client = getClient()
async function updateSelected (value: Ref<Person> | null | undefined) { const updateSelected = reduceCalls(async function (value: Ref<Person> | null | undefined) {
selected = value selected = value
? $personByIdStore.get(value) ?? (await client.findOne(contact.class.Person, { _id: value })) ? $personByIdStore.get(value) ?? (await client.findOne(contact.class.Person, { _id: value }))
: undefined : undefined
} })
$: updateSelected(value) $: void updateSelected(value)
const mgr = getFocusManager() const mgr = getFocusManager()

View File

@ -13,7 +13,9 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts" context="module"> <script lang="ts" context="module">
import contact, { AvatarProvider } from '@hcengineering/contact'
import { Client, Ref } from '@hcengineering/core' import { Client, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
const providers = new Map<string, AvatarProvider | null>() const providers = new Map<string, AvatarProvider | null>()
@ -29,15 +31,9 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import contact, { import { AvatarType, getAvatarProviderId, getFirstName, getLastName } from '@hcengineering/contact'
AvatarProvider,
AvatarType,
getFirstName,
getLastName,
getAvatarProviderId
} from '@hcengineering/contact'
import { Asset, getMetadata, getResource } from '@hcengineering/platform' import { Asset, getMetadata, getResource } from '@hcengineering/platform'
import { getBlobURL, getClient } from '@hcengineering/presentation' import { getBlobURL, reduceCalls } from '@hcengineering/presentation'
import { import {
AnySvelteComponent, AnySvelteComponent,
ColorDefinition, ColorDefinition,
@ -46,11 +42,11 @@
getPlatformAvatarColorByName, getPlatformAvatarColorByName,
getPlatformAvatarColorForTextDef, getPlatformAvatarColorForTextDef,
getPlatformColor, getPlatformColor,
themeStore, resizeObserver,
resizeObserver themeStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import AvatarIcon from './icons/Avatar.svelte'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import AvatarIcon from './icons/Avatar.svelte'
export let avatar: string | null | undefined = undefined export let avatar: string | null | undefined = undefined
export let name: string | null | undefined = undefined export let name: string | null | undefined = undefined
@ -89,12 +85,16 @@
return lastFirst ? lname + fname : fname + lname return lastFirst ? lname + fname : fname + lname
} }
async function update (size: IconSize, avatar?: string | null, direct?: Blob, name?: string | null) { const update = reduceCalls(async function (
size: IconSize,
avatar?: string | null,
direct?: Blob,
name?: string | null
) {
if (direct !== undefined) { if (direct !== undefined) {
getBlobURL(direct).then((blobURL) => { const blobURL = await getBlobURL(direct)
url = [blobURL] url = [blobURL]
avatarProvider = undefined avatarProvider = undefined
})
} else if (avatar) { } else if (avatar) {
const avatarProviderId = getAvatarProviderId(avatar) const avatarProviderId = getAvatarProviderId(avatar)
avatarProvider = avatarProviderId && (await getProvider(getClient(), avatarProviderId)) avatarProvider = avatarProviderId && (await getProvider(getClient(), avatarProviderId))
@ -116,8 +116,8 @@
url = undefined url = undefined
avatarProvider = undefined avatarProvider = undefined
} }
} })
$: update(size, avatar, direct, name) $: void update(size, avatar, direct, name)
$: srcset = url?.slice(1)?.join(', ') $: srcset = url?.slice(1)?.join(', ')

View File

@ -74,11 +74,11 @@
const client = getClient() const client = getClient()
async function updateSelected (value: Ref<Contact> | null | undefined) { async function updateSelected (value: Ref<Contact> | null | undefined): Promise<void> {
selected = value ? await client.findOne(_previewClass, { _id: value }) : undefined selected = value ? await client.findOne(_previewClass, { _id: value }) : undefined
} }
$: updateSelected(value) $: void updateSelected(value)
const mgr = getFocusManager() const mgr = getFocusManager()

View File

@ -84,7 +84,7 @@
/> />
{:else} {:else}
<Button <Button
id={'new-document'} id={'new-teamspace'}
icon={IconAdd} icon={IconAdd}
label={document.string.CreateTeamspace} label={document.string.CreateTeamspace}
justify={'left'} justify={'left'}

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Person } from '@hcengineering/contact' import { Person } from '@hcengineering/contact'
import { AssigneePopup, EmployeePresenter } from '@hcengineering/contact-resources' import { AssigneePopup, EmployeePresenter } from '@hcengineering/contact-resources'
import { Doc, Ref, SortingOrder } from '@hcengineering/core' import { Doc, Ref, SortingOrder, generateId } from '@hcengineering/core'
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation' import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@hcengineering/text-editor' import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@hcengineering/text-editor'
import { CheckBox, getEventPositionElement, showPopup } from '@hcengineering/ui' import { CheckBox, getEventPositionElement, showPopup } from '@hcengineering/ui'
@ -70,7 +70,7 @@
const title = node.textBetween(0, node.content.size, undefined, ' ') const title = node.textBetween(0, node.content.size, undefined, ' ')
const ops = client.apply('todo') const ops = client.apply('todo' + generateId())
if (todo !== undefined) { if (todo !== undefined) {
await ops.remove(todo) await ops.remove(todo)

View File

@ -42,9 +42,20 @@
async function click (evt: MouseEvent): Promise<void> { async function click (evt: MouseEvent): Promise<void> {
pressed = true pressed = true
showPopup(TagsEditorPopup, { object, targetClass }, getEventPopupPositionElement(evt), () => { showPopup(
TagsEditorPopup,
{ object, targetClass },
getEventPopupPositionElement(evt),
() => {
pressed = false pressed = false
}) },
undefined,
{
refId: 'TagsPopup',
category: 'popup',
overlay: true
}
)
} }
async function removeTag (tag: TagReference): Promise<void> { async function removeTag (tag: TagReference): Promise<void> {

View File

@ -34,10 +34,21 @@
} }
function click (evt: MouseEvent) { function click (evt: MouseEvent) {
showPopup(DraftTagsPopup, { targetClass, tags }, getEventPopupPositionElement(evt), undefined, (res) => { showPopup(
DraftTagsPopup,
{ targetClass, tags },
getEventPopupPositionElement(evt),
undefined,
(res) => {
tags = res tags = res
dispatch('change', tags) dispatch('change', tags)
}) },
{
refId: 'TagsPopup',
category: 'popup',
overlay: true
}
)
} }
</script> </script>

View File

@ -25,18 +25,19 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
async function addRef ({ title, color, _id: tag }: TagElement): Promise<void> { async function addRef ({ title, color, _id: tag }: TagElement): Promise<void> {
tags.push({ tags = [
...tags,
{
tag, tag,
title, title,
color color
}) }
tags = tags ]
dispatch('update', tags) dispatch('update', tags)
} }
async function removeTag (tag: TagElement): Promise<void> { async function removeTag (tag: TagElement): Promise<void> {
tags = tags.filter((t) => t.tag !== tag._id) tags = tags.filter((t) => t.tag !== tag._id)
tags = tags
dispatch('update', tags) dispatch('update', tags)
} }

View File

@ -40,7 +40,11 @@
} }
async function tagsHandler (evt: MouseEvent): Promise<void> { async function tagsHandler (evt: MouseEvent): Promise<void> {
showPopup(TagsEditorPopup, { object }, getEventPopupPositionElement(evt)) showPopup(TagsEditorPopup, { object }, getEventPopupPositionElement(evt), undefined, undefined, {
refId: 'TagsPopup',
category: 'popup',
overlay: true
})
} }
let allWidth: number let allWidth: number

View File

@ -24,7 +24,11 @@
}) })
async function tagsHandler (evt: MouseEvent): Promise<void> { async function tagsHandler (evt: MouseEvent): Promise<void> {
if (readonly) return if (readonly) return
showPopup(TagsEditorPopup, { object, targetClass }, getEventPopupPositionElement(evt)) showPopup(TagsEditorPopup, { object, targetClass }, getEventPopupPositionElement(evt), undefined, undefined, {
refId: 'TagsPopup',
category: 'popup',
overlay: true
})
} }
async function removeTag (tag: TagReference): Promise<void> { async function removeTag (tag: TagReference): Promise<void> {
if (tag !== undefined) await client.remove(tag) if (tag !== undefined) await client.remove(tag)

View File

@ -73,6 +73,11 @@
} }
} }
} }
},
{
refId: 'TagsPopup',
category: 'popup',
overlay: true
} }
) )
} }

View File

@ -79,6 +79,11 @@
if (result.action === 'add') addRef(result.tag) if (result.action === 'add') addRef(result.tag)
else if (result.action === 'remove') removeTag(items.filter((it) => it.tag === result.tag._id)[0]._id) else if (result.action === 'remove') removeTag(items.filter((it) => it.tag === result.tag._id)[0]._id)
} }
},
{
refId: 'TagsPopup',
category: 'popup',
overlay: true
} }
) )
} }

View File

@ -16,7 +16,7 @@
import { Class, Doc, Ref } from '@hcengineering/core' import { Class, Doc, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation' import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { findTagCategory, TagCategory, TagElement } from '@hcengineering/tags' import { TagCategory, TagElement, findTagCategory } from '@hcengineering/tags'
import { import {
Button, Button,
EditWithIcon, EditWithIcon,
@ -33,10 +33,10 @@
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tags from '../plugin' import tags from '../plugin'
import { createTagElement } from '../utils'
import CreateTagElement from './CreateTagElement.svelte' import CreateTagElement from './CreateTagElement.svelte'
import IconView from './icons/View.svelte' import IconView from './icons/View.svelte'
import IconViewHide from './icons/ViewHide.svelte' import IconViewHide from './icons/ViewHide.svelte'
import { createTagElement } from '../utils'
export let newElements: TagElement[] = [] export let newElements: TagElement[] = []
export let targetClass: Ref<Class<Doc>> export let targetClass: Ref<Class<Doc>>
@ -69,15 +69,12 @@
objects = newElements.concat(result) objects = newElements.concat(result)
}) })
async function onCreateTagElement (res: any): Promise<void> { async function onCreateTagElement (res: Ref<TagElement> | undefined | null): Promise<void> {
if (res === null) return if (res == null) return
setTimeout(() => { const tag = await getClient().findOne(tags.class.TagElement, { _id: res })
const tag = objects.findLast((e) => e._id === res)
if (tag === undefined) return
selected = [...selected, tag._id]
dispatch('update', { action: 'add', tag }) dispatch('update', { action: 'add', tag })
selected = [...selected, res]
inProcess = false inProcess = false
}, 1)
} }
async function createTagElementPopup (): Promise<void> { async function createTagElementPopup (): Promise<void> {

View File

@ -1,22 +1,14 @@
// Copyright © 2022 Hardcore Engineering Inc. // Copyright © 2022 Hardcore Engineering Inc.
import { import { type Class, type Data, type Doc, type DocumentQuery, type FindResult, type Ref } from '@hcengineering/core'
type Class,
type Data,
type Doc,
type DocumentQuery,
type FindResult,
generateId,
type Ref
} from '@hcengineering/core'
import { type Asset } from '@hcengineering/platform' import { type Asset } from '@hcengineering/platform'
import { type TagElement, type InitialKnowledge, type TagReference, type TagCategory } from '@hcengineering/tags' import { getClient } from '@hcengineering/presentation'
import { type InitialKnowledge, type TagCategory, type TagElement, type TagReference } from '@hcengineering/tags'
import { type ColorDefinition, getColorNumberByText } from '@hcengineering/ui' import { type ColorDefinition, getColorNumberByText } from '@hcengineering/ui'
import { type Filter } from '@hcengineering/view' import { type Filter } from '@hcengineering/view'
import { FilterQuery } from '@hcengineering/view-resources' import { FilterQuery } from '@hcengineering/view-resources'
import tags from './plugin'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import { getClient } from '@hcengineering/presentation' import tags from './plugin'
export function getTagStyle (color: ColorDefinition, selected = false): string { export function getTagStyle (color: ColorDefinition, selected = false): string {
return ` return `
@ -79,7 +71,7 @@ export async function createTagElement (
category?: Ref<TagCategory> | null, category?: Ref<TagCategory> | null,
description?: string | null, description?: string | null,
color?: number | null color?: number | null
): Promise<any> { ): Promise<Ref<TagElement>> {
const tagElement: Data<TagElement> = { const tagElement: Data<TagElement> = {
title, title,
description: description ?? '', description: description ?? '',
@ -89,6 +81,5 @@ export async function createTagElement (
} }
const client = getClient() const client = getClient()
const tagElementId = generateId() return await client.createDoc<TagElement>(tags.class.TagElement, tags.space.Tags, tagElement)
return await client.createDoc(tags.class.TagElement, tags.space.Tags, tagElement, tagElementId)
} }

View File

@ -72,8 +72,15 @@
let preference: ViewletPreference | undefined let preference: ViewletPreference | undefined
let documentIds: Ref<Task>[] = [] let documentIds: Ref<Task>[] = []
function updateResultQuery (search: string, documentIds: Ref<Task>[], doneStates: Status[]): void { function updateResultQuery (
search: string,
documentIds: Ref<Task>[],
doneStates: Status[],
mode: string | undefined
): void {
if (mode === 'assigned') {
resultQuery.status = { $nin: doneStates.map((it) => it._id) } resultQuery.status = { $nin: doneStates.map((it) => it._id) }
}
if (documentIds.length > 0) { if (documentIds.length > 0) {
resultQuery._id = { $in: documentIds } resultQuery._id = { $in: documentIds }
} }
@ -123,7 +130,7 @@
} }
} }
$: updateResultQuery(search, documentIds, doneStates) $: updateResultQuery(search, documentIds, doneStates, mode)
let viewlet: Viewlet | undefined let viewlet: Viewlet | undefined
@ -155,7 +162,7 @@
<SearchEdit <SearchEdit
bind:value={search} bind:value={search}
on:change={() => { on:change={() => {
updateResultQuery(search, documentIds, doneStates) updateResultQuery(search, documentIds, doneStates, mode)
}} }}
/> />
<div class="buttons-divider" /> <div class="buttons-divider" />

View File

@ -83,7 +83,7 @@
const client = getClient() const client = getClient()
async function updateQuery (query: DocumentQuery<Task>, selectedDoneStates: Set<Ref<Status>>): Promise<void> { function updateQuery (query: DocumentQuery<Task>, selectedDoneStates: Set<Ref<Status>>): void {
resConfig = updateConfig(config) resConfig = updateConfig(config)
const result = client.getHierarchy().clone(query) const result = client.getHierarchy().clone(query)
if (state) { if (state) {
@ -164,7 +164,14 @@
<TabList items={itemsDS} bind:selected={selectedDS} multiselect on:select={handleDoneSelect} /> <TabList items={itemsDS} bind:selected={selectedDS} multiselect on:select={handleDoneSelect} />
</ScrollerBar> </ScrollerBar>
{:else} {:else}
<StatesBar bind:state {space} gap={'none'} on:change={() => updateQuery(query, selectedDoneStates)} /> <StatesBar
bind:state
{space}
gap={'none'}
on:change={() => {
updateQuery(query, selectedDoneStates)
}}
/>
{/if} {/if}
</div> </div>
<div class="statustableview-container"> <div class="statustableview-container">

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import core, { IdMap, Ref, Status, StatusCategory } from '@hcengineering/core' import core, { IdMap, Ref, Status, StatusCategory } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient, reduceCalls } from '@hcengineering/presentation'
import task, { Project, ProjectType } from '@hcengineering/task' import task, { Project, ProjectType } from '@hcengineering/task'
import { import {
ColorDefinition, ColorDefinition,
@ -23,8 +23,8 @@
getPlatformColorDef, getPlatformColorDef,
themeStore themeStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { typeStore } from '../..'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { typeStore } from '../..'
export let value: Status | undefined export let value: Status | undefined
export let shouldShowTooltip: boolean = false export let shouldShowTooltip: boolean = false
@ -36,8 +36,16 @@
let category: StatusCategory | undefined let category: StatusCategory | undefined
let type: ProjectType | undefined = undefined let type: ProjectType | undefined = undefined
$: updateCategory(value) const update = reduceCalls(async function (
$: getType(space, $typeStore) value: Status | undefined,
space: Ref<Project>,
typeStore: IdMap<ProjectType>
) {
await updateCategory(value)
await getType(space, typeStore)
})
$: void update(value, space, $typeStore)
$: viewState = getViewState(type, value) $: viewState = getViewState(type, value)
@ -56,7 +64,7 @@
} }
} }
async function updateCategory (value: Status | undefined) { async function updateCategory (value: Status | undefined): Promise<void> {
if (value === undefined) return if (value === undefined) return
category = await client.findOne(core.class.StatusCategory, { _id: value.category }) category = await client.findOne(core.class.StatusCategory, { _id: value.category })
} }

View File

@ -16,7 +16,7 @@
<script lang="ts"> <script lang="ts">
import core, { IdMap, Ref, Status, StatusCategory } from '@hcengineering/core' import core, { IdMap, Ref, Status, StatusCategory } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient, reduceCalls } from '@hcengineering/presentation'
import task, { Project, ProjectType, TaskType } from '@hcengineering/task' import task, { Project, ProjectType, TaskType } from '@hcengineering/task'
import { import {
ColorDefinition, ColorDefinition,
@ -91,13 +91,13 @@
) )
$: void updateCategory(value) $: void updateCategory(value)
async function updateCategory (value: Status | undefined): Promise<void> { const updateCategory = reduceCalls(async function (value: Status | undefined): Promise<void> {
if (value === undefined) { if (value === undefined) {
category = undefined category = undefined
return return
} }
category = await client.findOne(core.class.StatusCategory, { _id: value.category }) category = await client.findOne(core.class.StatusCategory, { _id: value.category })
} })
const categoryIcons = { const categoryIcons = {
[task.statusCategory.UnStarted]: IconBacklog, [task.statusCategory.UnStarted]: IconBacklog,

View File

@ -35,13 +35,17 @@
const spaceQ = createQuery() const spaceQ = createQuery()
let templates: MessageTemplate[] = [] let templates: MessageTemplate[] = []
let selected: Ref<MessageTemplate> | undefined let selected: Ref<MessageTemplate> | undefined
let newTemplate: Data<MessageTemplate> | undefined = undefined
let title: string = ''
let message: string = ''
let newTemplate: boolean = false
query.query(templatesPlugin.class.MessageTemplate, {}, (t) => { query.query(templatesPlugin.class.MessageTemplate, {}, (t) => {
templates = t templates = t
if (templates.findIndex((t) => t._id === selected) === -1) { if (selected !== undefined && templates.findIndex((t) => t._id === selected) === -1) {
selected = undefined selected = undefined
newTemplate = undefined newTemplate = false
} }
}) })
@ -60,28 +64,28 @@
let mode = Mode.View let mode = Mode.View
async function addTemplate (): Promise<void> { async function addTemplate (): Promise<void> {
newTemplate = { title = ''
title: '', message = ''
message: '' newTemplate = true
}
mode = Mode.Create mode = Mode.Create
} }
async function saveNewTemplate (): Promise<void> { async function saveNewTemplate (): Promise<void> {
if (newTemplate === undefined) { if (!newTemplate) {
return return
} }
if (mode === Mode.Create) { if (mode === Mode.Create) {
if (newTemplate.title.trim().length > 0 && space !== undefined) { if (title.trim().length > 0 && space !== undefined) {
const ref = await client.createDoc(templatesPlugin.class.MessageTemplate, space, newTemplate) const ref = await client.createDoc(templatesPlugin.class.MessageTemplate, space, {
title,
message
})
selected = ref selected = ref
} }
} else if (selected !== undefined) { } else if (selected !== undefined) {
await client.updateDoc( await client.updateDoc(templatesPlugin.class.MessageTemplate, templatesPlugin.space.Templates, selected, {
templatesPlugin.class.MessageTemplate, title,
templatesPlugin.space.Templates, message
selected, })
newTemplate
)
} }
mode = Mode.View mode = Mode.View
} }
@ -92,7 +96,7 @@
textEditor.submit() textEditor.submit()
} }
const updateTemplate = (evt: any) => { const updateTemplate = (evt: any) => {
newTemplate = { title: newTemplate?.title ?? '', message: evt.detail } message = evt.detail
} }
function addField (ev: MouseEvent) { function addField (ev: MouseEvent) {
@ -184,7 +188,9 @@
indent indent
on:click={() => { on:click={() => {
selected = t._id selected = t._id
newTemplate = { title: t.title, message: t.message } title = t.title
message = t.message
newTemplate = true
mode = Mode.View mode = Mode.View
}} }}
selected={selected === t._id} selected={selected === t._id}
@ -223,18 +229,18 @@
</div> </div>
<div class="text-lg caption-color"> <div class="text-lg caption-color">
{#if mode !== Mode.View} {#if mode !== Mode.View}
<EditBox bind:value={newTemplate.title} placeholder={templatesPlugin.string.TemplatePlaceholder} /> <EditBox bind:value={title} placeholder={templatesPlugin.string.TemplatePlaceholder} />
{:else} {:else}
{newTemplate.title} {title}
{/if} {/if}
</div> </div>
<div class="separator" /> <div class="separator" />
{#if mode !== Mode.View} {#if mode !== Mode.View}
<StyledTextEditor bind:content={newTemplate.message} bind:this={textEditor} on:value={updateTemplate}> <StyledTextEditor bind:content={message} bind:this={textEditor} on:value={updateTemplate}>
<div class="flex flex-reverse flex-grow"> <div class="flex flex-reverse flex-grow">
<div class="ml-2"> <div class="ml-2">
<Button <Button
disabled={newTemplate.title.trim().length === 0} disabled={title.trim().length === 0}
kind={'primary'} kind={'primary'}
label={templatesPlugin.string.SaveTemplate} label={templatesPlugin.string.SaveTemplate}
on:click={saveNewTemplate} on:click={saveNewTemplate}
@ -245,7 +251,7 @@
label={templatesPlugin.string.Cancel} label={templatesPlugin.string.Cancel}
on:click={() => { on:click={() => {
if (mode === Mode.Create) { if (mode === Mode.Create) {
newTemplate = undefined newTemplate = false
} }
mode = Mode.View mode = Mode.View
}} }}
@ -256,7 +262,7 @@
</StyledTextEditor> </StyledTextEditor>
{:else} {:else}
<div class="text"> <div class="text">
<MessageViewer message={newTemplate.message} /> <MessageViewer {message} />
</div> </div>
<div class="flex flex-reverse"> <div class="flex flex-reverse">
<Button <Button

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { ActionIcon, IconAdd, showPopup, ModernEditbox } from '@hcengineering/ui' import { ActionIcon, IconAdd, showPopup, ModernEditbox } from '@hcengineering/ui'
import { SortingOrder, getCurrentAccount } from '@hcengineering/core' import { SortingOrder, generateId, getCurrentAccount } from '@hcengineering/core'
import { PersonAccount } from '@hcengineering/contact' import { PersonAccount } from '@hcengineering/contact'
import { ToDoPriority } from '@hcengineering/time' import { ToDoPriority } from '@hcengineering/time'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
@ -19,7 +19,7 @@
name = name.trim() name = name.trim()
if (name.length === 0) return if (name.length === 0) return
description = description?.trim() ?? '' description = description?.trim() ?? ''
const ops = client.apply('todo') const ops = client.apply('todo' + generateId())
const latestTodo = await ops.findOne( const latestTodo = await ops.findOne(
time.class.ToDo, time.class.ToDo,
{ {

View File

@ -55,7 +55,7 @@
async function saveToDo (): Promise<void> { async function saveToDo (): Promise<void> {
loading = true loading = true
const ops = client.apply('todo') const ops = client.apply('todo-' + generateId())
const latestTodo = await ops.findOne( const latestTodo = await ops.findOne(
time.class.ToDo, time.class.ToDo,
{ {
@ -239,8 +239,8 @@
<Component <Component
is={tagsPlugin.component.DraftTagsEditor} is={tagsPlugin.component.DraftTagsEditor}
props={{ tags, targetClass: time.class.ToDo }} props={{ tags, targetClass: time.class.ToDo }}
on:change={(e) => { on:update={(e) => {
tags = e.detail tags = [...e.detail]
}} }}
/> />
</div> </div>

View File

@ -63,7 +63,7 @@
$: from = getFrom(currentDate) $: from = getFrom(currentDate)
$: to = getTo(currentDate) $: to = getTo(currentDate)
function update (calendars: Calendar[]) { function update (calendars: Calendar[]): void {
q.query<Event>( q.query<Event>(
calendar.class.Event, calendar.class.Event,
{ space: { $in: calendars.map((p) => p._id) } }, { space: { $in: calendars.map((p) => p._id) } },

View File

@ -41,7 +41,8 @@
MultipleDraftController, MultipleDraftController,
SpaceSelector, SpaceSelector,
createQuery, createQuery,
getClient getClient,
reduceCalls
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags' import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { TaskType, makeRank } from '@hcengineering/task' import { TaskType, makeRank } from '@hcengineering/task'

View File

@ -18,7 +18,7 @@
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker' import { Component } from '@hcengineering/tracker'
import type { ButtonKind, ButtonSize, LabelAndProps, SelectPopupValueType } from '@hcengineering/ui' import type { ButtonKind, ButtonSize, LabelAndProps, SelectPopupValueType } from '@hcengineering/ui'
import { Button, ButtonShape, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui' import { Button, ButtonShape, SelectPopup, eventToHTMLElement, showPopup, PopupResult } from '@hcengineering/ui'
import tracker from '../../plugin' import tracker from '../../plugin'
import ComponentPresenter from './ComponentPresenter.svelte' import ComponentPresenter from './ComponentPresenter.svelte'
@ -57,12 +57,10 @@
} }
) )
$: handleSelectedComponentIdUpdated(value, rawComponents) const handleSelectedComponentIdUpdated = (
const handleSelectedComponentIdUpdated = async (
newComponentId: Ref<Component> | null | undefined, newComponentId: Ref<Component> | null | undefined,
components: Component[] components: Component[]
) => { ): void => {
if (newComponentId === null || newComponentId === undefined) { if (newComponentId === null || newComponentId === undefined) {
selectedComponent = undefined selectedComponent = undefined
@ -72,6 +70,8 @@
selectedComponent = components.find((it) => it._id === newComponentId) selectedComponent = components.find((it) => it._id === newComponentId)
} }
$: handleSelectedComponentIdUpdated(value, rawComponents)
function getComponentInfo (rawComponents: Component[], sp: Component | undefined): SelectPopupValueType[] { function getComponentInfo (rawComponents: Component[], sp: Component | undefined): SelectPopupValueType[] {
return [ return [
...(isAllowUnset ...(isAllowUnset
@ -100,17 +100,25 @@
let components: SelectPopupValueType[] = [] let components: SelectPopupValueType[] = []
$: components = getComponentInfo(rawComponents, selectedComponent) $: components = getComponentInfo(rawComponents, selectedComponent)
let selectPopupResult: PopupResult | undefined
// We need update SelectPopup when it active
$: selectPopupResult?.update({ value: components })
const handleComponentEditorOpened = async (event: MouseEvent): Promise<void> => { const handleComponentEditorOpened = async (event: MouseEvent): Promise<void> => {
event.stopPropagation() event.stopPropagation()
if (!isEditable) { if (!isEditable) {
return return
} }
showPopup( selectPopupResult = showPopup(
SelectPopup, SelectPopup,
{ value: components, placeholder: popupPlaceholder, searchable: true }, { value: components, placeholder: popupPlaceholder, searchable: true },
eventToHTMLElement(event), eventToHTMLElement(event),
onChange (result: any) => {
onChange?.(result)
selectPopupResult = undefined
}
) )
} }
</script> </script>

View File

@ -11,7 +11,7 @@
} from '@hcengineering/core' } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus } from '@hcengineering/tracker' import { Issue, IssueStatus } from '@hcengineering/tracker'
import { Label, ticker, Row } from '@hcengineering/ui' import { Label, Row, ticker } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources' import { statusStore } from '@hcengineering/view-resources'
import tracker from '../../plugin' import tracker from '../../plugin'
import Duration from './Duration.svelte' import Duration from './Duration.svelte'
@ -39,7 +39,7 @@
) )
let displaySt: WithTime[] = [] let displaySt: WithTime[] = []
async function updateStatus (txes: Tx[], statuses: IdMap<IssueStatus>, _: number): Promise<void> { function updateStatus (txes: Tx[], statuses: IdMap<IssueStatus>, _: number): void {
const result: WithTime[] = [] const result: WithTime[] = []
let current: Ref<IssueStatus> | undefined let current: Ref<IssueStatus> | undefined

View File

@ -17,7 +17,7 @@
import { IntlString, getEmbeddedLabel, translate } from '@hcengineering/platform' import { IntlString, getEmbeddedLabel, translate } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import { Milestone } from '@hcengineering/tracker' import { Milestone } from '@hcengineering/tracker'
import type { ButtonKind, ButtonSize, LabelAndProps } from '@hcengineering/ui' import type { ButtonKind, ButtonSize, LabelAndProps, PopupResult } from '@hcengineering/ui'
import { Button, ButtonShape, Label, SelectPopup, eventToHTMLElement, showPopup, themeStore } from '@hcengineering/ui' import { Button, ButtonShape, Label, SelectPopup, eventToHTMLElement, showPopup, themeStore } from '@hcengineering/ui'
import tracker from '../../plugin' import tracker from '../../plugin'
import { milestoneStatusAssets } from '../../types' import { milestoneStatusAssets } from '../../types'
@ -96,19 +96,25 @@
$: milestones = getMilestoneInfo(rawMilestones, selectedMilestone) $: milestones = getMilestoneInfo(rawMilestones, selectedMilestone)
let milestonePopup: PopupResult | undefined
const handleMilestoneEditorOpened = async (event: MouseEvent): Promise<void> => { const handleMilestoneEditorOpened = async (event: MouseEvent): Promise<void> => {
event.stopPropagation() event.stopPropagation()
if (!isEditable) { if (!isEditable) {
return return
} }
showPopup( milestonePopup = showPopup(
SelectPopup, SelectPopup,
{ value: milestones, placeholder: popupPlaceholder, searchable: true }, { value: milestones, placeholder: popupPlaceholder, searchable: true },
eventToHTMLElement(event), eventToHTMLElement(event),
onChange (evt) => {
onChange?.(evt)
milestonePopup = undefined
}
) )
} }
$: milestonePopup?.update({ value: milestones })
</script> </script>
{#if isAction} {#if isAction}

View File

@ -16,7 +16,7 @@
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import core, { Doc, Hierarchy, Ref, Space, TxRemoveDoc } from '@hcengineering/core' import core, { Doc, Hierarchy, Ref, Space, TxRemoveDoc } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { addTxListener, contextStore, getClient } from '@hcengineering/presentation' import { addTxListener, contextStore, getClient, reduceCalls } from '@hcengineering/presentation'
import { AnyComponent, Component } from '@hcengineering/ui' import { AnyComponent, Component } from '@hcengineering/ui'
import { Action, ViewContextType } from '@hcengineering/view' import { Action, ViewContextType } from '@hcengineering/view'
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
@ -237,11 +237,12 @@
} }
let presenter: AnyComponent | undefined let presenter: AnyComponent | undefined
async function updatePreviewPresenter (doc?: Doc): Promise<void> { const updatePreviewPresenter = reduceCalls(async function (doc?: Doc): Promise<void> {
presenter = doc !== undefined ? await getObjectPreview(client, Hierarchy.mixinOrClass(doc)) : undefined const r = doc !== undefined ? await getObjectPreview(client, Hierarchy.mixinOrClass(doc)) : undefined
} presenter = r
})
$: updatePreviewPresenter($previewDocument) $: void updatePreviewPresenter($previewDocument)
</script> </script>
<svelte:window on:keydown={handleKeys} /> <svelte:window on:keydown={handleKeys} />

View File

@ -13,42 +13,55 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { WithLookup, Doc, Ref, SearchResultDoc } from '@hcengineering/core' import core, {
Doc,
Ref,
SearchResultDoc,
Tx,
TxBuilder,
TxWorkspaceEvent,
WithLookup,
WorkspaceEvent,
coreId
} from '@hcengineering/core'
import { getResource, translate } from '@hcengineering/platform' import { getResource, translate } from '@hcengineering/platform'
import { import {
type SearchItem, ActionContext,
type ObjectSearchCategory,
SearchResult, SearchResult,
addTxListener,
createQuery, createQuery,
getClient, getClient,
ActionContext, reduceCalls,
searchFor removeTxListener,
searchFor,
type ObjectSearchCategory,
type SearchItem
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import ui, { import ui, {
Button, Button,
closePopup,
Component, Component,
Icon, Icon,
IconArrowLeft, IconArrowLeft,
Label, Label,
deviceOptionsStore,
capitalizeFirstLetter,
formatKey,
themeStore,
ListView, ListView,
resizeObserver capitalizeFirstLetter,
closePopup,
deviceOptionsStore,
formatKey,
resizeObserver,
themeStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { Action, ActionCategory, ViewContext } from '@hcengineering/view' import { Action, ActionCategory, ViewContext } from '@hcengineering/view'
import { createEventDispatcher, onMount, tick } from 'svelte'
import { filterActions, getSelection } from '../actions' import { filterActions, getSelection } from '../actions'
import view from '../plugin' import view from '../plugin'
import { focusStore, selectionStore } from '../selection' import { focusStore, selectionStore } from '../selection'
import { openDoc } from '../utils' import { openDoc } from '../utils'
import ObjectPresenter from './ObjectPresenter.svelte' import ObjectPresenter from './ObjectPresenter.svelte'
import { createEventDispatcher, tick } from 'svelte'
import { contextStore } from '@hcengineering/presentation'
import ChevronDown from './icons/ChevronDown.svelte' import ChevronDown from './icons/ChevronDown.svelte'
import ChevronUp from './icons/ChevronUp.svelte' import ChevronUp from './icons/ChevronUp.svelte'
import { contextStore } from '@hcengineering/presentation'
export let viewContext: ViewContext | undefined = $contextStore.getLastContext() export let viewContext: ViewContext | undefined = $contextStore.getLastContext()
@ -98,7 +111,7 @@
const client = getClient() const client = getClient()
async function getSupportedActions (actions: Array<WithLookup<Action>>): Promise<void> { const getSupportedActions = reduceCalls(async (actions: Array<WithLookup<Action>>): Promise<void> => {
const docs = getSelection($focusStore, $selectionStore) const docs = getSelection($focusStore, $selectionStore)
let fActions: Array<WithLookup<Action>> = actions let fActions: Array<WithLookup<Action>> = actions
@ -125,11 +138,11 @@
fActions = await filterVisibleActions(fActions, docs) fActions = await filterVisibleActions(fActions, docs)
// Sort by category. // Sort by category.
supportedActions = fActions.sort((a, b) => a.category.localeCompare(b.category)) supportedActions = fActions.sort((a, b) => a.category.localeCompare(b.category))
} })
$: void getSupportedActions(actions) $: void getSupportedActions(actions)
async function filterSearchActions (actions: Array<WithLookup<Action>>, search: string): Promise<void> { const filterSearchActions = reduceCalls(async (actions: Array<WithLookup<Action>>, search: string): Promise<void> => {
const res: Array<WithLookup<Action>> = [] const res: Array<WithLookup<Action>> = []
let preparedSearch = search.trim().toLowerCase() let preparedSearch = search.trim().toLowerCase()
if (preparedSearch.charAt(0) === '/') { if (preparedSearch.charAt(0) === '/') {
@ -146,7 +159,7 @@
} else { } else {
filteredActions = actions filteredActions = actions
} }
} })
$: void filterSearchActions(supportedActions, search) $: void filterSearchActions(supportedActions, search)
let selection = 0 let selection = 0
@ -241,16 +254,32 @@
let items: SearchActionItem[] = [] let items: SearchActionItem[] = []
async function updateItems (query: string, filteredActions: Array<WithLookup<Action>>): Promise<void> { const updateItems = reduceCalls(async (query: string, filteredActions: Array<WithLookup<Action>>): Promise<void> => {
let searchItems: SearchItem[] = [] let searchItems: SearchItem[] = []
if (query !== '' && query.indexOf('/') !== 0) { if (query !== '' && query.indexOf('/') !== 0) {
searchItems = (await searchFor('spotlight', query)).items searchItems = (await searchFor('spotlight', query)).items
} }
items = packSearchAndActions(searchItems, filteredActions) items = packSearchAndActions(searchItems, filteredActions)
} })
$: void updateItems(search, filteredActions) $: void updateItems(search, filteredActions)
function txListener (tx: Tx): void {
if (tx._class === core.class.TxWorkspaceEvent) {
const evt = tx as TxWorkspaceEvent
if (evt.event === WorkspaceEvent.IndexingUpdate) {
void updateItems(search, filteredActions)
}
}
}
onMount(() => {
addTxListener(txListener)
return () => {
removeTxListener(txListener)
}
})
let textHTML: HTMLInputElement let textHTML: HTMLInputElement
let phTraslate: string = '' let phTraslate: string = ''
let autoFocus = !$deviceOptionsStore.isMobile let autoFocus = !$deviceOptionsStore.isMobile

View File

@ -25,13 +25,14 @@
KeyedAttribute, KeyedAttribute,
createQuery, createQuery,
getClient, getClient,
hasResource hasResource,
reduceCalls
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import { AnyComponent, Button, Component, IconMixin, IconMoreH } from '@hcengineering/ui' import { AnyComponent, Button, Component, IconMixin, IconMoreH } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { DocNavLink, ParentsNavigator, getDocLabel, getDocMixins, showMenu, getDocAttrsInfo } from '..' import { DocNavLink, ParentsNavigator, getDocAttrsInfo, getDocLabel, getDocMixins, showMenu } from '..'
import { getCollectionCounter } from '../utils' import { getCollectionCounter } from '../utils'
import DocAttributeBar from './DocAttributeBar.svelte' import DocAttributeBar from './DocAttributeBar.svelte'
@ -68,7 +69,7 @@
const query = createQuery() const query = createQuery()
$: updateQuery(_id, _class) $: updateQuery(_id, _class)
function updateQuery (_id: Ref<Doc>, _class: Ref<Class<Doc>>) { function updateQuery (_id: Ref<Doc>, _class: Ref<Class<Doc>>): void {
if (_id && _class) { if (_id && _class) {
query.query(_class, { _id }, (result) => { query.query(_class, { _id }, (result) => {
object = result[0] object = result[0]
@ -146,12 +147,12 @@
$: editorFooter = getEditorFooter(_class, object) $: editorFooter = getEditorFooter(_class, object)
$: void getEditorOrDefault(realObjectClass, _id) const getEditorOrDefault = reduceCalls(async function (_class: Ref<Class<Doc>>, _id: Ref<Doc>): Promise<void> {
async function getEditorOrDefault (_class: Ref<Class<Doc>>, _id: Ref<Doc>): Promise<void> {
await updateKeys() await updateKeys()
mainEditor = getEditor(_class) mainEditor = getEditor(_class)
} })
$: void getEditorOrDefault(realObjectClass, _id)
let title: string | undefined = undefined let title: string | undefined = undefined
let rawTitle: string = '' let rawTitle: string = ''

View File

@ -27,7 +27,7 @@
getObjectValue getObjectValue
} from '@hcengineering/core' } from '@hcengineering/core'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { createQuery, getClient, updateAttribute } from '@hcengineering/presentation' import { createQuery, getClient, reduceCalls, updateAttribute } from '@hcengineering/presentation'
import ui, { import ui, {
Button, Button,
CheckBox, CheckBox,
@ -117,7 +117,7 @@
: { ...(options?.sort ?? {}), [sortKey]: sortOrder } : { ...(options?.sort ?? {}), [sortKey]: sortOrder }
} }
async function update ( const update = reduceCalls(async function (
_class: Ref<Class<Doc>>, _class: Ref<Class<Doc>>,
query: DocumentQuery<Doc>, query: DocumentQuery<Doc>,
sortKey: string | string[], sortKey: string | string[],
@ -143,8 +143,8 @@
) )
? 1 ? 1
: 0 : 0
} })
$: update(_class, query, _sortKey, sortOrder, lookup, limit, options) $: void update(_class, query, _sortKey, sortOrder, lookup, limit, options)
$: dispatch('content', objects) $: dispatch('content', objects)
@ -276,36 +276,31 @@
let model: AttributeModel[] | undefined let model: AttributeModel[] | undefined
let modelOptions: BuildModelOptions | undefined let modelOptions: BuildModelOptions | undefined
$: updateModelOptions(client, _class, config, lookup) const updateModelOptions = reduceCalls(async function updateModelOptions (
async function updateModelOptions (
client: TxOperations, client: TxOperations,
_class: Ref<Class<Doc>>, _class: Ref<Class<Doc>>,
config: Array<string | BuildModelKey>, config: Array<string | BuildModelKey>,
lookup?: Lookup<Doc> lookup?: Lookup<Doc>
) { ): Promise<void> {
const newModelOpts = { client, _class, keys: config, lookup } const newModelOpts = { client, _class, keys: config, lookup }
if (modelOptions == null || !deepEqual(modelOptions, newModelOpts)) { if (modelOptions == null || !deepEqual(modelOptions, newModelOpts)) {
modelOptions = newModelOpts modelOptions = newModelOpts
await build(modelOptions) await build(modelOptions)
} }
} })
$: void updateModelOptions(client, _class, config, lookup)
let buildIndex = 0 async function build (modelOptions: BuildModelOptions): Promise<void> {
async function build (modelOptions: BuildModelOptions) {
isBuildingModel = true isBuildingModel = true
const idx = ++buildIndex
const res = await buildModel(modelOptions) const res = await buildModel(modelOptions)
if (buildIndex === idx) {
model = res model = res
}
isBuildingModel = false isBuildingModel = false
} }
function contextHandler (object: Doc, row: number): (ev: MouseEvent) => void { function contextHandler (object: Doc, row: number): (ev: MouseEvent) => void {
return (ev) => { return (ev) => {
if (!readonly) { if (!readonly) {
showContextMenu(ev, object, row) void showContextMenu(ev, object, row)
} }
} }
} }

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import { Class, Doc, DocumentQuery, Ref, Space, getCurrentAccount } from '@hcengineering/core' import { Class, Doc, DocumentQuery, Ref, Space, getCurrentAccount } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient, reduceCalls } from '@hcengineering/presentation'
import { Button, IconAdd, eventToHTMLElement, getCurrentLocation, showPopup } from '@hcengineering/ui' import { Button, IconAdd, eventToHTMLElement, getCurrentLocation, showPopup } from '@hcengineering/ui'
import { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view' import { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
@ -80,7 +80,7 @@
} }
} }
async function makeQuery (query: DocumentQuery<Doc>, filters: Filter[]): Promise<void> { const makeQuery = reduceCalls(async (query: DocumentQuery<Doc>, filters: Filter[]): Promise<void> => {
const newQuery = hierarchy.clone(query) const newQuery = hierarchy.clone(query)
for (let i = 0; i < filters.length; i++) { for (let i = 0; i < filters.length; i++) {
const filter = filters[i] const filter = filters[i]
@ -141,7 +141,7 @@
} }
} }
dispatch('change', newQuery) dispatch('change', newQuery)
} })
$: makeQuery(query, $filterStore) $: makeQuery(query, $filterStore)

View File

@ -35,7 +35,7 @@
} }
}) })
function load (_class: Ref<Class<Doc>> | undefined) { function load (_class: Ref<Class<Doc>> | undefined): void {
const key = getFilterKey(_class) const key = getFilterKey(_class)
const items = localStorage.getItem(key) const items = localStorage.getItem(key)
if (items !== null) { if (items !== null) {
@ -43,7 +43,7 @@
} }
} }
function save (_class: Ref<Class<Doc>> | undefined, p: Filter[]) { function save (_class: Ref<Class<Doc>> | undefined, p: Filter[]): void {
const key = getFilterKey(_class) const key = getFilterKey(_class)
localStorage.setItem(key, JSON.stringify(p)) localStorage.setItem(key, JSON.stringify(p))
} }
@ -52,7 +52,7 @@
save(_class, p) save(_class, p)
}) })
function onChange (e: Filter | undefined) { function onChange (e: Filter | undefined): void {
if (e !== undefined) setFilters([e]) if (e !== undefined) setFilters([e])
} }
@ -60,7 +60,7 @@
_class = undefined _class = undefined
}) })
function add (e: MouseEvent) { function add (e: MouseEvent): void {
const target = eventToHTMLElement(e) const target = eventToHTMLElement(e)
showPopup( showPopup(
FilterTypePopup, FilterTypePopup,

View File

@ -18,13 +18,13 @@
Doc, Doc,
DocumentQuery, DocumentQuery,
FindOptions, FindOptions,
RateLimiter,
Ref, Ref,
Space, Space,
RateLimiter,
mergeQueries mergeQueries
} from '@hcengineering/core' } from '@hcengineering/core'
import { IntlString, getResource } from '@hcengineering/platform' import { IntlString, getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient, reduceCalls } from '@hcengineering/presentation'
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui' import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
import { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption, Viewlet } from '@hcengineering/view' import { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
@ -70,9 +70,12 @@
$: resultOptions = { ...options, lookup, ...(orderBy !== undefined ? { sort: { [orderBy[0]]: orderBy[1] } } : {}) } $: resultOptions = { ...options, lookup, ...(orderBy !== undefined ? { sort: { [orderBy[0]]: orderBy[1] } } : {}) }
let resultQuery: DocumentQuery<Doc> = query let resultQuery: DocumentQuery<Doc> = query
$: void getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => {
const update = reduceCalls(async function (query: DocumentQuery<Doc>, viewOptions: ViewOptions) {
const p = await getResultQuery(query, viewOptionsConfig, viewOptions)
resultQuery = mergeQueries(p, query) resultQuery = mergeQueries(p, query)
}) })
$: void update(query, viewOptions)
$: queryNoLookup = noLookup(resultQuery) $: queryNoLookup = noLookup(resultQuery)

View File

@ -26,7 +26,7 @@
Space Space
} from '@hcengineering/core' } from '@hcengineering/core'
import { getResource, IntlString } from '@hcengineering/platform' import { getResource, IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient, reduceCalls } from '@hcengineering/presentation'
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui' import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
import { import {
AttributeModel, AttributeModel,
@ -100,18 +100,15 @@
CategoryQuery.remove(queryId) CategoryQuery.remove(queryId)
}) })
function update (): void { const updateCategories = reduceCalls(
void updateCategories(_class, space, docs, groupByKey, viewOptions, viewOptionsConfig) async (
}
async function updateCategories (
_class: Ref<Class<Doc>>, _class: Ref<Class<Doc>>,
space: Ref<Space> | undefined, space: Ref<Space> | undefined,
docs: Doc[], docs: Doc[],
groupByKey: string, groupByKey: string,
viewOptions: ViewOptions, viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined viewOptionsModel: ViewOptionModel[] | undefined
): Promise<void> { ): Promise<void> => {
categories = await getCategories(client, _class, space, docs, groupByKey) categories = await getCategories(client, _class, space, docs, groupByKey)
if (level === 0) { if (level === 0) {
for (const viewOption of viewOptionsModel ?? []) { for (const viewOption of viewOptionsModel ?? []) {
@ -128,29 +125,31 @@
} }
} }
} }
)
function update (): void {
void updateCategories(_class, space, docs, groupByKey, viewOptions, viewOptionsConfig)
}
let itemModels = new Map<Ref<Class<Doc>>, AttributeModel[]>() let itemModels = new Map<Ref<Class<Doc>>, AttributeModel[]>()
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void { const getHeader = reduceCalls(async function (_class: Ref<Class<Doc>>, groupByKey: string): Promise<void> {
if (groupByKey === noCategory) { if (groupByKey === noCategory) {
headerComponent = undefined headerComponent = undefined
} else { } else {
getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p)) await getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p))
}
} }
})
let headerComponent: AttributeModel | undefined let headerComponent: AttributeModel | undefined
$: getHeader(_class, groupByKey) $: void getHeader(_class, groupByKey)
let updateCounter = 0
let configurationsVersion = 0 let configurationsVersion = 0
async function buildModels ( const buildModels = reduceCalls(async function (
_class: Ref<Class<Doc>>, _class: Ref<Class<Doc>>,
config: Array<string | BuildModelKey>, config: Array<string | BuildModelKey>,
configurations?: Record<Ref<Class<Doc>>, Viewlet['config']> | undefined configurations?: Record<Ref<Class<Doc>>, Viewlet['config']> | undefined
): Promise<void> { ): Promise<void> {
const id = ++updateCounter
updateCounter = id
const newItemModels = new Map<Ref<Class<Doc>>, AttributeModel[]>() const newItemModels = new Map<Ref<Class<Doc>>, AttributeModel[]>()
const entries = Object.entries(configurations ?? []) const entries = Object.entries(configurations ?? [])
for (const [k, v] of entries) { for (const [k, v] of entries) {
@ -164,9 +163,7 @@
newItemModels.set(_class, res) newItemModels.set(_class, res)
} }
if (id === updateCounter) {
itemModels = newItemModels itemModels = newItemModels
configurationsVersion = updateCounter
for (const [, v] of Object.entries(newItemModels)) { for (const [, v] of Object.entries(newItemModels)) {
// itemModels = itemModels // itemModels = itemModels
;(v as AttributeModel[]).forEach((m: AttributeModel) => { ;(v as AttributeModel[]).forEach((m: AttributeModel) => {
@ -178,10 +175,10 @@
} }
}) })
} }
} configurationsVersion = configurationsVersion + 1
} })
$: buildModels(_class, config, configurations) $: void buildModels(_class, config, configurations)
function getInitIndex (categories: any, i: number): number { function getInitIndex (categories: any, i: number): number {
let res = initIndex let res = initIndex

View File

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import contact from '@hcengineering/contact'
import { Ref, getCurrentAccount, toIdMap } from '@hcengineering/core' import { Ref, getCurrentAccount, toIdMap } from '@hcengineering/core'
import { copyTextToClipboard, createQuery, getClient } from '@hcengineering/presentation' import { copyTextToClipboard, createQuery, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
@ -6,18 +7,19 @@
Action, Action,
IconAdd, IconAdd,
Location, Location,
eventToHTMLElement,
location,
navigate,
showPopup,
SelectPopup, SelectPopup,
eventToHTMLElement,
getEventPopupPositionElement, getEventPopupPositionElement,
getLocation,
getPopupPositionElement, getPopupPositionElement,
location,
locationToUrl, locationToUrl,
getLocation navigate,
showPopup
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view, { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view' import view, { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view'
import { import {
EditBoxPopup,
TreeItem, TreeItem,
TreeNode, TreeNode,
activeViewlet, activeViewlet,
@ -28,14 +30,13 @@
setActiveViewletId, setActiveViewletId,
setFilters, setFilters,
setViewOptions, setViewOptions,
viewOptionStore, viewOptionStore
EditBoxPopup
} from '@hcengineering/view-resources' } from '@hcengineering/view-resources'
import { Application } from '@hcengineering/workbench' import { Application } from '@hcengineering/workbench'
import copy from 'fast-copy' import copy from 'fast-copy'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import contact from '@hcengineering/contact' import TodoCheck from './icons/TodoCheck.svelte'
import task from '../../../task/lib' import TodoUncheck from './icons/TodoUncheck.svelte'
export let currentApplication: Application | undefined export let currentApplication: Application | undefined
@ -99,7 +100,7 @@
async function switchPublicAction (object: FilteredView, originalEvent: MouseEvent | undefined): Promise<Action[]> { async function switchPublicAction (object: FilteredView, originalEvent: MouseEvent | undefined): Promise<Action[]> {
return [ return [
{ {
icon: object.sharable ? task.icon.TodoCheck : task.icon.TodoUnCheck, icon: object.sharable ? TodoCheck : TodoUncheck,
label: view.string.PublicView, label: view.string.PublicView,
action: async (ctx: any, evt: Event) => { action: async (ctx: any, evt: Event) => {
await client.update(object, { sharable: !object.sharable }) await client.update(object, { sharable: !object.sharable })

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import core, { Class, Doc, Ref, Space, WithLookup } from '@hcengineering/core' import core, { Class, Doc, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient, reduceCalls } from '@hcengineering/presentation'
import { AnyComponent, Component, resolvedLocationStore } from '@hcengineering/ui' import { AnyComponent, Component, resolvedLocationStore } from '@hcengineering/ui'
import view, { ViewOptions, Viewlet } from '@hcengineering/view' import view, { ViewOptions, Viewlet } from '@hcengineering/view'
import { import {
@ -55,9 +55,7 @@
$: active = $activeViewlet[key] $: active = $activeViewlet[key]
$: update(active, currentSpace, currentView?.class) const update = reduceCalls(async function update (
async function update (
active: Ref<Viewlet> | null, active: Ref<Viewlet> | null,
currentSpace?: Ref<Space>, currentSpace?: Ref<Space>,
attachTo?: Ref<Class<Doc>> attachTo?: Ref<Class<Doc>>
@ -88,7 +86,9 @@
} }
_class = attachTo _class = attachTo
} }
} })
$: void update(active, currentSpace, currentView?.class)
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
async function getHeader (_class: Ref<Class<Space>>): Promise<AnyComponent | undefined> { async function getHeader (_class: Ref<Class<Space>>): Promise<AnyComponent | undefined> {
@ -97,7 +97,7 @@
if (headerMixin?.header == null && clazz.extends != null) return await getHeader(clazz.extends) if (headerMixin?.header == null && clazz.extends != null) return await getHeader(clazz.extends)
return headerMixin.header return headerMixin.header
} }
function setViewlet (e: CustomEvent<WithLookup<Viewlet>>) { function setViewlet (e: CustomEvent<WithLookup<Viewlet>>): void {
viewlet = e.detail viewlet = e.detail
} }
</script> </script>

View File

@ -20,7 +20,14 @@
import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification' import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources' import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { IntlString, broadcastEvent, getMetadata, getResource } from '@hcengineering/platform' import { IntlString, broadcastEvent, getMetadata, getResource } from '@hcengineering/platform'
import { ActionContext, ComponentExtensions, createQuery, getClient, isAdminUser } from '@hcengineering/presentation' import {
ActionContext,
ComponentExtensions,
createQuery,
getClient,
isAdminUser,
reduceCalls
} from '@hcengineering/presentation'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import support, { SupportStatus, supportLink } from '@hcengineering/support' import support, { SupportStatus, supportLink } from '@hcengineering/support'
import { import {
@ -163,18 +170,17 @@
let hasNotificationsFn: ((data: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>) | undefined = let hasNotificationsFn: ((data: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>) | undefined =
undefined undefined
let hasInboxNotifications = false let hasInboxNotifications = false
let syncPromise: Promise<void> | undefined = undefined
let locUpdate = 0
getResource(notification.function.HasInboxNotifications).then((f) => { void getResource(notification.function.HasInboxNotifications).then((f) => {
hasNotificationsFn = f hasNotificationsFn = f
}) })
$: hasNotificationsFn?.($inboxNotificationsByContextStore).then((res) => { $: void hasNotificationsFn?.($inboxNotificationsByContextStore).then((res) => {
hasInboxNotifications = res hasInboxNotifications = res
}) })
const doSyncLoc = async (loc: Location, iteration: number): Promise<void> => { const doSyncLoc = reduceCalls(async (loc: Location): Promise<void> => {
console.log('do sync', JSON.stringify(loc), $location.path)
if (workspaceId !== $location.path[1]) { if (workspaceId !== $location.path[1]) {
// Switch of workspace // Switch of workspace
return return
@ -182,19 +188,15 @@
closeTooltip() closeTooltip()
closePopup() closePopup()
await syncLoc(loc, iteration) await syncLoc(loc)
await updateWindowTitle(loc) await updateWindowTitle(loc)
checkOnHide() checkOnHide()
syncPromise = undefined console.log('do sync-end', JSON.stringify(loc), $location.path)
} })
onDestroy( onDestroy(
location.subscribe((loc) => { location.subscribe((loc) => {
locUpdate++ void doSyncLoc(loc)
if (syncPromise !== undefined) {
void syncPromise.then(() => doSyncLoc(loc, locUpdate))
} else {
syncPromise = doSyncLoc(loc, locUpdate)
}
}) })
) )
@ -294,15 +296,12 @@
return loc return loc
} }
async function syncLoc (loc: Location, iteration: number): Promise<void> { async function syncLoc (loc: Location): Promise<void> {
const originalLoc = JSON.stringify(loc) const originalLoc = JSON.stringify(loc)
if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) { if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) {
// resolve short links // resolve short links
const resolvedLoc = await resolveShortLink(loc) const resolvedLoc = await resolveShortLink(loc)
if (locUpdate !== iteration) {
return
}
if (resolvedLoc !== undefined && !areLocationsEqual(loc, resolvedLoc.loc)) { if (resolvedLoc !== undefined && !areLocationsEqual(loc, resolvedLoc.loc)) {
loc = mergeLoc(loc, resolvedLoc) loc = mergeLoc(loc, resolvedLoc)
} }
@ -334,9 +333,6 @@
let len = 3 let len = 3
if (spaceRef !== undefined && specialRef !== undefined) { if (spaceRef !== undefined && specialRef !== undefined) {
const spaceObj = await client.findOne<Space>(core.class.Space, { _id: spaceRef }) const spaceObj = await client.findOne<Space>(core.class.Space, { _id: spaceRef })
if (locUpdate !== iteration) {
return
}
if (spaceObj !== undefined) { if (spaceObj !== undefined) {
loc.path[3] = spaceRef loc.path[3] = spaceRef
loc.path[4] = specialRef loc.path[4] = specialRef
@ -355,13 +351,7 @@
clear(1) clear(1)
currentAppAlias = app currentAppAlias = app
currentApplication = await client.findOne<Application>(workbench.class.Application, { alias: app }) currentApplication = await client.findOne<Application>(workbench.class.Application, { alias: app })
if (locUpdate !== iteration) {
return
}
navigatorModel = await buildNavModel(client, currentApplication) navigatorModel = await buildNavModel(client, currentApplication)
if (locUpdate !== iteration) {
return
}
} }
if ( if (
@ -395,9 +385,6 @@
currentSpecial = space currentSpecial = space
} else { } else {
await updateSpace(space) await updateSpace(space)
if (locUpdate !== iteration) {
return
}
setSpaceSpecial(special) setSpaceSpecial(special)
} }
} }
@ -425,6 +412,7 @@
if (props.length >= 3) { if (props.length >= 3) {
const doc = await client.findOne<Doc>(props[2] as Ref<Class<Doc>>, { _id: props[1] as Ref<Doc> }) const doc = await client.findOne<Doc>(props[2] as Ref<Class<Doc>>, { _id: props[1] as Ref<Doc> })
if (doc !== undefined) { if (doc !== undefined) {
const provider = ListSelectionProvider.Find(doc._id) const provider = ListSelectionProvider.Find(doc._id)
updateFocus({ updateFocus({

View File

@ -0,0 +1,11 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path
d="M8,14.5c-3.6,0-6.5-2.9-6.5-6.5S4.4,1.5,8,1.5s6.5,2.9,6.5,6.5S11.6,14.5,8,14.5z M8,2.5C5,2.5,2.5,5,2.5,8 c0,3,2.5,5.5,5.5,5.5c3,0,5.5-2.5,5.5-5.5C13.5,5,11,2.5,8,2.5z"
/>
<polygon points="7.4,10.7 5,8.4 5.7,7.6 7.3,9.3 10.3,5.7 11,6.3 " />
</svg>

View File

@ -0,0 +1,10 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path
d="M8,14.5c-3.6,0-6.5-2.9-6.5-6.5S4.4,1.5,8,1.5s6.5,2.9,6.5,6.5S11.6,14.5,8,14.5z M8,2.5C5,2.5,2.5,5,2.5,8 c0,3,2.5,5.5,5.5,5.5c3,0,5.5-2.5,5.5-5.5C13.5,5,11,2.5,8,2.5z"
/>
</svg>

View File

@ -877,7 +877,13 @@ export async function createWorkspace (
try { try {
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
const wsId = getWorkspaceId(workspaceInfo.workspace, productId) const wsId = getWorkspaceId(workspaceInfo.workspace, productId)
if (initWS !== undefined && (await getWorkspaceById(db, productId, initWS)) !== null) {
// We should not try to clone INIT_WS into INIT_WS during it's creation.
if (
initWS !== undefined &&
(await getWorkspaceById(db, productId, initWS)) !== null &&
initWS !== workspaceInfo.workspace
) {
// Just any valid model for transactor to be able to function // Just any valid model for transactor to be able to function
await ( await (
await initModel(ctx, getTransactor(), wsId, txes, [], ctxModellogger, async (value) => { await initModel(ctx, getTransactor(), wsId, txes, [], ctxModellogger, async (value) => {

View File

@ -207,10 +207,19 @@ export class FullTextIndex implements WithFind {
const indexedDocMap = new Map<Ref<Doc>, IndexedDoc>() const indexedDocMap = new Map<Ref<Doc>, IndexedDoc>()
for (const doc of docs) { for (const doc of docs) {
if (doc._class.some((cl) => this.hierarchy.isDerived(cl, baseClass))) { if (
doc._class != null &&
Array.isArray(doc._class) &&
doc._class.some((cl) => this.hierarchy.isDerived(cl, baseClass))
) {
ids.add(doc.id) ids.add(doc.id)
indexedDocMap.set(doc.id, doc) indexedDocMap.set(doc.id, doc)
} }
if (doc._class !== null && !Array.isArray(doc._class) && this.hierarchy.isDerived(doc._class, baseClass)) {
ids.add(doc.id)
indexedDocMap.set(doc.id, doc)
}
if (doc.attachedTo != null) { if (doc.attachedTo != null) {
if (doc.attachedToClass != null && this.hierarchy.isDerived(doc.attachedToClass, baseClass)) { if (doc.attachedToClass != null && this.hierarchy.isDerived(doc.attachedToClass, baseClass)) {
if (this.hierarchy.isDerived(doc.attachedToClass, baseClass)) { if (this.hierarchy.isDerived(doc.attachedToClass, baseClass)) {

View File

@ -245,19 +245,20 @@ export async function upgradeModel (
const migrateClient = new MigrateClientImpl(db, hierarchy, modelDb, logger) const migrateClient = new MigrateClientImpl(db, hierarchy, modelDb, logger)
const states = await migrateClient.find<MigrationState>(DOMAIN_MIGRATION, { _class: core.class.MigrationState }) const states = await migrateClient.find<MigrationState>(DOMAIN_MIGRATION, { _class: core.class.MigrationState })
const migrateState = new Map( const sts = Array.from(groupByArray(states, (it) => it.plugin).entries())
Array.from(groupByArray(states, (it) => it.plugin).entries()).map((it) => [ const migrateState = new Map(sts.map((it) => [it[0], new Set(it[1].map((q) => q.state))]))
it[0],
new Set(it[1].map((q) => q.state))
])
)
migrateClient.migrateState = migrateState migrateClient.migrateState = migrateState
await ctx.with('migrate', {}, async () => { await ctx.with('migrate', {}, async () => {
let i = 0 let i = 0
for (const op of migrateOperations) { for (const op of migrateOperations) {
const t = Date.now() const t = Date.now()
try {
await op[1].migrate(migrateClient, logger) await op[1].migrate(migrateClient, logger)
} catch (err: any) {
logger.error(`error during migrate: ${op[0]} ${err.message}`, err)
throw err
}
logger.log('migrate:', { workspaceId: workspaceId.name, operation: op[0], time: Date.now() - t }) logger.log('migrate:', { workspaceId: workspaceId.name, operation: op[0], time: Date.now() - t })
await progress(20 + ((100 / migrateOperations.length) * i * 20) / 100) await progress(20 + ((100 / migrateOperations.length) * i * 20) / 100)
i++ i++

View File

@ -19,8 +19,9 @@ test.describe('Collaborative tests for Application', () => {
const vacancyName = 'Software Engineer' const vacancyName = 'Software Engineer'
let talentName: TalentName let talentName: TalentName
// open second page // open second page
const userSecondPage = await getSecondPage(browser) const { page: userSecondPage, context } = await getSecondPage(browser)
try {
await test.step('User1. Add collaborators and comment from user1', async () => { await test.step('User1. Add collaborators and comment from user1', async () => {
const navigationMenuPage = new NavigationMenuPage(page) const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonApplications.click() await navigationMenuPage.buttonApplications.click()
@ -87,5 +88,9 @@ test.describe('Collaborative tests for Application', () => {
const applicationsDetailsPage = new ApplicationsDetailsPage(page) const applicationsDetailsPage = new ApplicationsDetailsPage(page)
await applicationsDetailsPage.checkCommentExist('Test Comment from user2') await applicationsDetailsPage.checkCommentExist('Test Comment from user2')
}) })
} finally {
await userSecondPage.close()
await context.close()
}
}) })
}) })

View File

@ -1,9 +1,9 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { generateId, getSecondPage, PlatformSetting, PlatformURI } from '../utils'
import { NewIssue } from '../model/tracker/types'
import { IssuesPage } from '../model/tracker/issues-page'
import { LeftSideMenuPage } from '../model/left-side-menu-page' import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { IssuesDetailsPage } from '../model/tracker/issues-details-page' import { IssuesDetailsPage } from '../model/tracker/issues-details-page'
import { IssuesPage } from '../model/tracker/issues-page'
import { NewIssue } from '../model/tracker/types'
import { generateId, getSecondPage, PlatformSetting, PlatformURI } from '../utils'
test.use({ test.use({
storageState: PlatformSetting storageState: PlatformSetting
@ -22,7 +22,7 @@ test.describe('Collaborative test for issue', () => {
priority: 'Urgent', priority: 'Urgent',
assignee: 'Appleseed John', assignee: 'Appleseed John',
createLabel: true, createLabel: true,
labels: `CREATE-ISSUE-${generateId()}`, labels: `CREATE-ISSUE-${generateId(5)}`,
component: 'No component', component: 'No component',
estimation: '2', estimation: '2',
milestone: 'No Milestone', milestone: 'No Milestone',
@ -31,7 +31,8 @@ test.describe('Collaborative test for issue', () => {
} }
// open second page // open second page
const userSecondPage = await getSecondPage(browser) const { page: userSecondPage, context } = await getSecondPage(browser)
try {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished() await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
// create a new issue by first user // create a new issue by first user
@ -62,7 +63,10 @@ test.describe('Collaborative test for issue', () => {
milestone: 'Milestone', milestone: 'Milestone',
estimation: '2h' estimation: '2h'
}) })
} finally {
await userSecondPage.close() await userSecondPage.close()
await context.close()
}
}) })
test('Issues status can be changed by another users', async ({ page, browser }) => { test('Issues status can be changed by another users', async ({ page, browser }) => {
@ -72,15 +76,18 @@ test.describe('Collaborative test for issue', () => {
} }
// open second page // open second page
const userSecondPage = await getSecondPage(browser) const { page: userSecondPage, context } = await getSecondPage(browser)
try {
if (userSecondPage.url() !== `${PlatformURI}/workbench/sanity-ws/tracker/`) {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished() await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
}
const issuesPageSecond = new IssuesPage(userSecondPage) const issuesPageSecond = new IssuesPage(userSecondPage)
await issuesPageSecond.linkSidebarAll.click() await issuesPageSecond.linkSidebarAll.click()
await issuesPageSecond.modelSelectorAll.click() await issuesPageSecond.modelSelectorAll.click()
// change status // change status
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const issuesPage = new IssuesPage(page) const issuesPage = new IssuesPage(page)
await issuesPage.linkSidebarAll.click() await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorBacklog.click() await issuesPage.modelSelectorBacklog.click()
@ -107,7 +114,10 @@ test.describe('Collaborative test for issue', () => {
...issue, ...issue,
status: 'In Progress' status: 'In Progress'
}) })
} finally {
await userSecondPage.close() await userSecondPage.close()
await context.close()
}
}) })
test('First user change assignee, second user should see assigned issue', async ({ page, browser }) => { test('First user change assignee, second user should see assigned issue', async ({ page, browser }) => {
@ -118,7 +128,8 @@ test.describe('Collaborative test for issue', () => {
} }
// open second page // open second page
const userSecondPage = await getSecondPage(browser) const { page: userSecondPage, context } = await getSecondPage(browser)
try {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished() await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
await test.step(`user1. change assignee to ${newAssignee}`, async () => { await test.step(`user1. change assignee to ${newAssignee}`, async () => {
@ -156,6 +167,9 @@ test.describe('Collaborative test for issue', () => {
const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage) const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage)
await issuesDetailsPageSecond.checkIssue({ ...issue }) await issuesDetailsPageSecond.checkIssue({ ...issue })
}) })
} finally {
await userSecondPage.close() await userSecondPage.close()
await context.close()
}
}) })
}) })

View File

@ -1,10 +1,10 @@
import { expect, test } from '@playwright/test' import { expect, test } from '@playwright/test'
import { generateId, PlatformSetting, PlatformURI } from '../utils' import { DocumentContentPage } from '../model/documents/document-content-page'
import { DocumentsPage } from '../model/documents/documents-page'
import { NewDocument } from '../model/documents/types' import { NewDocument } from '../model/documents/types'
import { LeftSideMenuPage } from '../model/left-side-menu-page' import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { DocumentsPage } from '../model/documents/documents-page'
import { DocumentContentPage } from '../model/documents/document-content-page'
import { PublicLinkPopup } from '../model/tracker/public-link-popup' import { PublicLinkPopup } from '../model/tracker/public-link-popup'
import { generateId, PlatformSetting, PlatformURI } from '../utils'
test.describe('Documents link tests', () => { test.describe('Documents link tests', () => {
test('Document public link revoke', async ({ browser }) => { test('Document public link revoke', async ({ browser }) => {
@ -15,6 +15,7 @@ test.describe('Documents link tests', () => {
const newContext = await browser.newContext({ storageState: PlatformSetting }) const newContext = await browser.newContext({ storageState: PlatformSetting })
const page = await newContext.newPage() const page = await newContext.newPage()
try {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished() await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
const leftSideMenuPage = new LeftSideMenuPage(page) const leftSideMenuPage = new LeftSideMenuPage(page)
@ -39,6 +40,7 @@ test.describe('Documents link tests', () => {
const clearSession = await browser.newContext() const clearSession = await browser.newContext()
const clearPage = await clearSession.newPage() const clearPage = await clearSession.newPage()
try {
await test.step('Check guest access to the document', async () => { await test.step('Check guest access to the document', async () => {
await clearPage.goto(link) await clearPage.goto(link)
@ -56,5 +58,13 @@ test.describe('Documents link tests', () => {
await clearPage.goto(link) await clearPage.goto(link)
await expect(clearPage.locator('div.antiPopup > h1')).toHaveText('Public link was revoked') await expect(clearPage.locator('div.antiPopup > h1')).toHaveText('Public link was revoked')
}) })
} finally {
await clearPage.close()
await clearSession.close()
}
} finally {
await page.close()
await newContext.close()
}
}) })
}) })

View File

@ -130,7 +130,8 @@ test.describe('Documents tests', () => {
}) })
await test.step('User2. Add content second user', async () => { await test.step('User2. Add content second user', async () => {
const userSecondPage = await getSecondPage(browser) const { page: userSecondPage, context } = await getSecondPage(browser)
try {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished() await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
const leftSideMenuPageSecond = new LeftSideMenuPage(userSecondPage) const leftSideMenuPageSecond = new LeftSideMenuPage(userSecondPage)
@ -146,6 +147,10 @@ test.describe('Documents tests', () => {
content = await documentContentPageSecond.addContentToTheNewLine(contentSecondUser) content = await documentContentPageSecond.addContentToTheNewLine(contentSecondUser)
await documentContentPageSecond.checkContent(content) await documentContentPageSecond.checkContent(content)
} finally {
await userSecondPage.close()
await context.close()
}
}) })
await test.step('User1. Check final content', async () => { await test.step('User1. Check final content', async () => {

View File

@ -52,6 +52,7 @@ export class CommonPage {
.locator('div.popup form[id="tags:string:AddTag"] input[placeholder="Please type description here"]') .locator('div.popup form[id="tags:string:AddTag"] input[placeholder="Please type description here"]')
.fill(description) .fill(description)
await page.locator('div.popup form[id="tags:string:AddTag"] button[type="submit"]').click() await page.locator('div.popup form[id="tags:string:AddTag"] button[type="submit"]').click()
await page.locator('div.popup form[id="tags:string:AddTag"]').waitFor({ state: 'hidden' })
} }
async selectAssignee (page: Page, name: string): Promise<void> { async selectAssignee (page: Page, name: string): Promise<void> {

View File

@ -27,6 +27,7 @@ export class DocumentContentPage extends CommonPage {
async addContentToTheNewLine (newContent: string): Promise<string> { async addContentToTheNewLine (newContent: string): Promise<string> {
await expect(this.inputContent).toBeVisible() await expect(this.inputContent).toBeVisible()
await expect(this.inputContent).toHaveJSProperty('contentEditable', 'true')
await this.inputContent.pressSequentially(`\n${newContent}`) await this.inputContent.pressSequentially(`\n${newContent}`)
const endContent = await this.inputContent.textContent() const endContent = await this.inputContent.textContent()
if (endContent == null) { if (endContent == null) {
@ -42,6 +43,7 @@ export class DocumentContentPage extends CommonPage {
async updateDocumentTitle (title: string): Promise<void> { async updateDocumentTitle (title: string): Promise<void> {
await this.buttonDocumentTitle.fill(title) await this.buttonDocumentTitle.fill(title)
await this.buttonDocumentTitle.blur()
} }
async addRandomLines (count: number, lineLength: number = 36): Promise<void> { async addRandomLines (count: number, lineLength: number = 36): Promise<void> {

View File

@ -117,9 +117,10 @@ export class PlanningPage extends CalendarPage {
if (data.createLabel) { if (data.createLabel) {
await this.pressCreateButtonSelectPopup(this.page) await this.pressCreateButtonSelectPopup(this.page)
await this.addNewTagPopup(this.page, data.labels, 'Tag from createNewIssue') await this.addNewTagPopup(this.page, data.labels, 'Tag from createNewIssue')
} await this.page.locator('.popup#TagsPopup').press('Escape')
} else {
await this.checkFromDropdownWithSearch(this.page, data.labels) await this.checkFromDropdownWithSearch(this.page, data.labels)
await (popup ? this.buttonPopupCreateAddLabel.press('Escape') : this.buttonPanelCreateAddLabel.press('Escape')) }
} }
if (data.slots != null) { if (data.slots != null) {
let index = 0 let index = 0

View File

@ -31,7 +31,7 @@ export class IssuesDetailsPage extends CommonTrackerPage {
super(page) super(page)
this.page = page this.page = page
this.inputTitle = page.locator('div.popupPanel-body input[type="text"]') this.inputTitle = page.locator('div.popupPanel-body input[type="text"]')
this.inputDescription = page.locator('div.popupPanel-body div.textInput p') this.inputDescription = page.locator('div.popupPanel-body div.textInput div.tiptap')
this.buttonStatus = page.locator('//span[text()="Status"]/../button[1]//span') this.buttonStatus = page.locator('//span[text()="Status"]/../button[1]//span')
this.buttonPriority = page.locator('//span[text()="Priority"]/../button[2]//span') this.buttonPriority = page.locator('//span[text()="Priority"]/../button[2]//span')
this.buttonAssignee = page.locator('(//span[text()="Assignee"]/../div/button)[2]') this.buttonAssignee = page.locator('(//span[text()="Assignee"]/../div/button)[2]')
@ -170,6 +170,7 @@ export class IssuesDetailsPage extends CommonTrackerPage {
async addToDescription (description: string): Promise<void> { async addToDescription (description: string): Promise<void> {
const existDescription = await this.inputDescription.textContent() const existDescription = await this.inputDescription.textContent()
await expect(this.inputDescription).toHaveJSProperty('contentEditable', 'true')
await this.inputDescription.fill(`${existDescription}\n${description}`) await this.inputDescription.fill(`${existDescription}\n${description}`)
} }

View File

@ -110,11 +110,10 @@ export class IssuesPage extends CommonTrackerPage {
if (data.createLabel) { if (data.createLabel) {
await this.pressCreateButtonSelectPopup(this.page) await this.pressCreateButtonSelectPopup(this.page)
await this.addNewTagPopup(this.page, data.labels, 'Tag from createNewIssue') await this.addNewTagPopup(this.page, data.labels, 'Tag from createNewIssue')
await this.page.locator('.popup#TagsPopup').press('Escape')
} else { } else {
await this.checkFromDropdown(this.page, data.labels) await this.checkFromDropdown(this.page, data.labels)
} }
await this.inputPopupCreateNewIssueTitle.press('Escape')
await this.inputPopupCreateNewIssueTitle.click({ force: true })
} }
if (data.component != null) { if (data.component != null) {
await this.buttonPopupCreateNewIssueComponent.click() await this.buttonPopupCreateNewIssueComponent.click()
@ -211,10 +210,14 @@ export class IssuesPage extends CommonTrackerPage {
} }
async checkAllIssuesByPriority (priorityName: string): Promise<void> { async checkAllIssuesByPriority (priorityName: string): Promise<void> {
await expect(async () => {
for await (const locator of iterateLocator(this.issuesList)) { for await (const locator of iterateLocator(this.issuesList)) {
const href = await locator.locator('div.priority-container use').getAttribute('href') const href = await locator.locator('div.priority-container use').getAttribute('href')
expect(href).toContain(priorityName) expect(href, { message: `Should contain ${priorityName} but it is ${href}` }).toContain(priorityName)
} }
}).toPass({
timeout: 15000
})
} }
async getIssueId (issueLabel: string, position: number = 0): Promise<string> { async getIssueId (issueLabel: string, position: number = 0): Promise<string> {

View File

@ -16,8 +16,8 @@ export async function prepareNewIssueStep (page: Page, issue: NewIssue): Promise
export async function prepareNewIssueWithOpenStep (page: Page, issue: NewIssue): Promise<string> { export async function prepareNewIssueWithOpenStep (page: Page, issue: NewIssue): Promise<string> {
return await test.step('Prepare document', async () => { return await test.step('Prepare document', async () => {
const issuesPage = new IssuesPage(page) const issuesPage = new IssuesPage(page)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorAll.click() await issuesPage.modelSelectorAll.click()
await issuesPage.createNewIssue(issue) await issuesPage.createNewIssue(issue)
await issuesPage.searchIssueByName(issue.title) await issuesPage.searchIssueByName(issue.title)
await issuesPage.openIssueByName(issue.title) await issuesPage.openIssueByName(issue.title)

View File

@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test' import { expect, test } from '@playwright/test'
import { navigate } from './tracker.utils'
import { generateId, PlatformSetting, PlatformURI, fillSearch } from '../utils' import { generateId, PlatformSetting, PlatformURI, fillSearch } from '../utils'
import { LeftSideMenuPage } from '../model/left-side-menu-page' import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { TrackerNavigationMenuPage } from '../model/tracker/tracker-navigation-menu-page' import { TrackerNavigationMenuPage } from '../model/tracker/tracker-navigation-menu-page'
@ -18,8 +17,6 @@ test.describe('Tracker component tests', () => {
test('create-component-issue', async ({ page }) => { test('create-component-issue', async ({ page }) => {
await page.click('[id="app-tracker\\:string\\:TrackerApplication"]') await page.click('[id="app-tracker\\:string\\:TrackerApplication"]')
await navigate(page)
await page.click('text=Components') await page.click('text=Components')
await expect(page).toHaveURL( await expect(page).toHaveURL(
`${PlatformURI}/workbench/sanity-ws/tracker/tracker%3Aproject%3ADefaultProject/components` `${PlatformURI}/workbench/sanity-ws/tracker/tracker%3Aproject%3ADefaultProject/components`

View File

@ -235,6 +235,7 @@ test.describe('Tracker filters tests', () => {
await leftSideMenuPage.buttonTracker.click() await leftSideMenuPage.buttonTracker.click()
const issuesPage = new IssuesPage(page) const issuesPage = new IssuesPage(page)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorAll.click() await issuesPage.modelSelectorAll.click()
for (const status of DEFAULT_STATUSES) { for (const status of DEFAULT_STATUSES) {

View File

@ -317,6 +317,7 @@ test.describe('Tracker issue tests', () => {
await trackerNavigationMenuPage.openIssuesForProject('Default') await trackerNavigationMenuPage.openIssuesForProject('Default')
const issuesPage = new IssuesPage(page) const issuesPage = new IssuesPage(page)
await issuesPage.searchIssueByName(commentIssue.title)
await issuesPage.checkCommentsCount(commentIssue.title, '1') await issuesPage.checkCommentsCount(commentIssue.title, '1')
await issuesPage.openCommentPopupForIssueByName(commentIssue.title) await issuesPage.openCommentPopupForIssueByName(commentIssue.title)
@ -327,6 +328,7 @@ test.describe('Tracker issue tests', () => {
await issuesPage.modelSelectorAll.click() await issuesPage.modelSelectorAll.click()
await issuesPage.searchIssueByName(commentIssue.title)
await issuesPage.checkCommentsCount(commentIssue.title, '2') await issuesPage.checkCommentsCount(commentIssue.title, '2')
}) })
}) })

View File

@ -17,6 +17,7 @@ test.describe('Tracker public link issues tests', () => {
await test.step('Get public link from popup', async () => { await test.step('Get public link from popup', async () => {
const newContext = await browser.newContext({ storageState: PlatformSetting }) const newContext = await browser.newContext({ storageState: PlatformSetting })
const page = await newContext.newPage() const page = await newContext.newPage()
try {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished() await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
const leftSideMenuPage = new LeftSideMenuPage(page) const leftSideMenuPage = new LeftSideMenuPage(page)
@ -28,6 +29,10 @@ test.describe('Tracker public link issues tests', () => {
const publicLinkPopup = new PublicLinkPopup(page) const publicLinkPopup = new PublicLinkPopup(page)
link = await publicLinkPopup.getPublicLink() link = await publicLinkPopup.getPublicLink()
} finally {
await page.close()
await newContext.close()
}
}) })
await test.step('Check guest access to the issue', async () => { await test.step('Check guest access to the issue', async () => {
@ -36,6 +41,7 @@ test.describe('Tracker public link issues tests', () => {
await clearSession.clearPermissions() await clearSession.clearPermissions()
const clearPage = await clearSession.newPage() const clearPage = await clearSession.newPage()
try {
await clearPage.goto(link) await clearPage.goto(link)
const clearIssuesDetailsPage = new IssuesDetailsPage(clearPage) const clearIssuesDetailsPage = new IssuesDetailsPage(clearPage)
@ -45,6 +51,10 @@ test.describe('Tracker public link issues tests', () => {
status: 'Backlog' status: 'Backlog'
}) })
expect(clearPage.url()).toContain('guest') expect(clearPage.url()).toContain('guest')
} finally {
await clearPage.close()
await clearSession.close()
}
}) })
}) })
@ -57,6 +67,7 @@ test.describe('Tracker public link issues tests', () => {
const newContext = await browser.newContext({ storageState: PlatformSetting }) const newContext = await browser.newContext({ storageState: PlatformSetting })
const page = await newContext.newPage() const page = await newContext.newPage()
let link: string let link: string
try {
await test.step('Get public link from popup', async () => { await test.step('Get public link from popup', async () => {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished() await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
@ -73,6 +84,7 @@ test.describe('Tracker public link issues tests', () => {
const clearSession = await browser.newContext() const clearSession = await browser.newContext()
const clearPage = await clearSession.newPage() const clearPage = await clearSession.newPage()
try {
await test.step('Check guest access to the issue', async () => { await test.step('Check guest access to the issue', async () => {
await clearPage.goto(link) await clearPage.goto(link)
@ -94,5 +106,13 @@ test.describe('Tracker public link issues tests', () => {
await clearPage.goto(link) await clearPage.goto(link)
await expect(clearPage.locator('div.antiPopup > h1')).toHaveText('Public link was revoked') await expect(clearPage.locator('div.antiPopup > h1')).toHaveText('Public link was revoked')
}) })
} finally {
await clearPage.close()
await clearSession.close()
}
} finally {
await page.close()
await newContext.close()
}
}) })
}) })

View File

@ -23,7 +23,7 @@ test.describe('Tracker sub-issues tests', () => {
}) })
test('create sub-issue', async ({ page }) => { test('create sub-issue', async ({ page }) => {
await navigate(page) await page.click('[id="app-tracker\\:string\\:TrackerApplication"]')
const props = { const props = {
name: `issue-${generateId(5)}`, name: `issue-${generateId(5)}`,

View File

@ -1,4 +1,4 @@
import { Browser, Locator, Page, expect } from '@playwright/test' import { Browser, BrowserContext, Locator, Page, expect } from '@playwright/test'
import { allure } from 'allure-playwright' import { allure } from 'allure-playwright'
export const PlatformURI = process.env.PLATFORM_URI as string export const PlatformURI = process.env.PLATFORM_URI as string
@ -62,9 +62,9 @@ export async function fillSearch (page: Page, search: string): Promise<Locator>
return searchBox return searchBox
} }
export async function getSecondPage (browser: Browser): Promise<Page> { export async function getSecondPage (browser: Browser): Promise<{ page: Page, context: BrowserContext }> {
const userSecondContext = await browser.newContext({ storageState: PlatformSettingSecond }) const userSecondContext = await browser.newContext({ storageState: PlatformSettingSecond })
return await userSecondContext.newPage() return { page: await userSecondContext.newPage(), context: userSecondContext }
} }
export function expectToContainsOrdered (val: Locator, text: string[], timeout?: number): Promise<void> { export function expectToContainsOrdered (val: Locator, text: string[], timeout?: number): Promise<void> {
const origIssuesExp = new RegExp('.*' + text.join('.*') + '.*') const origIssuesExp = new RegExp('.*' + text.join('.*') + '.*')