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 })
})
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 ?? {
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) => {
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 () => {},
logOperation: (operation, time, params) => {}

View File

@ -60,6 +60,15 @@ export interface ModelLogger {
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
*/
@ -68,6 +77,6 @@ export const consoleModelLogger: ModelLogger = {
console.log(msg, data)
},
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') {
key.preventDefault()
key.stopPropagation()
handleSelection(key, objects, selection)
void handleSelection(key, objects, selection)
}
}
const manager = createFocusManager()
@ -217,7 +217,7 @@
class="menu-item withList w-full flex-row-center"
disabled={readonly || isDeselectDisabled || loading}
on:click={() => {
handleSelection(undefined, objects, item)
void handleSelection(undefined, objects, item)
}}
>
<span class="label" class:disabled={readonly || isDeselectDisabled || loading}>

View File

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

View File

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

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import { Analytics } from '@hcengineering/analytics'
import core, {
TxOperations,
getCurrentAccount,
@ -51,7 +52,6 @@ import { onDestroy } from 'svelte'
import { type KeyedAttribute } from '..'
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
import plugin from './plugin'
import { Analytics } from '@hcengineering/analytics'
let liveQuery: LQ
let client: TxOperations & MeasureClient
@ -552,3 +552,42 @@ export function decodeTokenPayload (token: string): any {
export function isAdminUser (): boolean {
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.
-->
<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 { createEventDispatcher } from 'svelte'
import presentation, { type SearchItem, SearchResult, searchFor } from '@hcengineering/presentation'
import { SearchResultDoc } from '@hcengineering/core'
export let query: string = ''
@ -67,12 +67,12 @@
return false
}
async function updateItems (localQuery: string): Promise<void> {
const updateItems = reduceCalls(async function (localQuery: string): Promise<void> {
const r = await searchFor('mention', localQuery)
if (r.query === query) {
items = r.items
}
}
})
$: void updateItems(query)
</script>

View File

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

View File

@ -16,11 +16,11 @@
<script lang="ts">
import { onMount } from 'svelte'
import { deviceOptionsStore as deviceInfo, resizeObserver, testing } from '..'
import { fitPopupElement } from '../popups'
import type { AnySvelteComponent, PopupAlignment, PopupOptions, PopupPositionElement, DeviceOptions } from '../types'
import { CompAndProps, fitPopupElement } from '../popups'
import type { AnySvelteComponent, DeviceOptions, PopupAlignment, PopupOptions, PopupPositionElement } from '../types'
export let is: AnySvelteComponent
export let props: object
export let props: Record<string, any>
export let element: PopupAlignment | undefined
export let onClose: ((result: any) => void) | undefined
export let onUpdate: ((result: any) => void) | undefined
@ -29,11 +29,16 @@
export let top: boolean
export let close: () => void
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
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
interface PopupParams {
@ -81,7 +86,7 @@
close()
}
function escapeClose () {
function escapeClose (): void {
if (componentInstance?.canClose) {
if (!componentInstance.canClose()) return
}
@ -109,6 +114,8 @@
function handleKeydown (ev: KeyboardEvent) {
if (ev.key === 'Escape' && is && top) {
ev.preventDefault()
ev.stopPropagation()
escapeClose()
}
}
@ -268,6 +275,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
id={popup.options.refId}
class="popup {testing ? 'endShow' : showing === undefined ? 'endShow' : !showing ? 'preShow' : 'startShow'}"
class:testing
class:anim={(element === 'float' || element === 'centered') && !testing && !drag}

View File

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

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@
_class: Ref<Class<ActivityMessage>>,
lastViewedTimestamp?: Timestamp,
selectedMessageId?: Ref<ActivityMessage>
) {
): void {
if (dataProvider === undefined) {
// 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

View File

@ -41,13 +41,13 @@
let title: 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
})
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)) {
description = undefined
} else if (hierarchy.isDerived(_class, chunter.class.Channel)) {

View File

@ -30,7 +30,7 @@
$: 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)
}
</script>

View File

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

View File

@ -39,7 +39,7 @@
$: disabledRemoveFor = currentAccount._id !== object?.createdBy && creatorPersonRef ? [creatorPersonRef] : []
$: updateMembers(object)
function updateMembers (object: Channel | undefined) {
function updateMembers (object: Channel | undefined): void {
if (object === undefined) {
members = new Set()
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) {
return
}
@ -75,7 +75,7 @@
await Promise.all([leaveChannel(object, toLeave), joinChannel(object, toJoin)])
}
async function removeMember (ev: CustomEvent) {
async function removeMember (ev: CustomEvent): Promise<void> {
if (object === undefined) {
return
}
@ -97,7 +97,7 @@
await leaveChannel(object, accounts)
}
function openSelectUsersPopup () {
function openSelectUsersPopup (): void {
showPopup(
SelectUsersPopup,
{
@ -108,7 +108,7 @@
'top',
(result?: Ref<Person>[]) => {
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 { Class, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import presentation, { getClient, reduceCalls } from '@hcengineering/presentation'
import {
ActionIcon,
Button,
@ -76,13 +76,13 @@
const client = getClient()
async function updateSelected (value: Ref<Person> | null | undefined) {
const updateSelected = reduceCalls(async function (value: Ref<Person> | null | undefined) {
selected = value
? $personByIdStore.get(value) ?? (await client.findOne(contact.class.Person, { _id: value }))
: undefined
}
})
$: updateSelected(value)
$: void updateSelected(value)
const mgr = getFocusManager()

View File

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

View File

@ -74,11 +74,11 @@
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
}
$: updateSelected(value)
$: void updateSelected(value)
const mgr = getFocusManager()

View File

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

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Person } from '@hcengineering/contact'
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 { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@hcengineering/text-editor'
import { CheckBox, getEventPositionElement, showPopup } from '@hcengineering/ui'
@ -70,7 +70,7 @@
const title = node.textBetween(0, node.content.size, undefined, ' ')
const ops = client.apply('todo')
const ops = client.apply('todo' + generateId())
if (todo !== undefined) {
await ops.remove(todo)

View File

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

View File

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

View File

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

View File

@ -40,7 +40,11 @@
}
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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@
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)
const result = client.getHierarchy().clone(query)
if (state) {
@ -164,7 +164,14 @@
<TabList items={itemsDS} bind:selected={selectedDS} multiselect on:select={handleDoneSelect} />
</ScrollerBar>
{:else}
<StatesBar bind:state {space} gap={'none'} on:change={() => updateQuery(query, selectedDoneStates)} />
<StatesBar
bind:state
{space}
gap={'none'}
on:change={() => {
updateQuery(query, selectedDoneStates)
}}
/>
{/if}
</div>
<div class="statustableview-container">

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script lang="ts">
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 { ToDoPriority } from '@hcengineering/time'
import { getClient } from '@hcengineering/presentation'
@ -19,7 +19,7 @@
name = name.trim()
if (name.length === 0) return
description = description?.trim() ?? ''
const ops = client.apply('todo')
const ops = client.apply('todo' + generateId())
const latestTodo = await ops.findOne(
time.class.ToDo,
{

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@
} from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
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 tracker from '../../plugin'
import Duration from './Duration.svelte'
@ -39,7 +39,7 @@
)
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[] = []
let current: Ref<IssueStatus> | undefined

View File

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

View File

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

View File

@ -13,42 +13,55 @@
// limitations under the License.
-->
<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 {
type SearchItem,
type ObjectSearchCategory,
ActionContext,
SearchResult,
addTxListener,
createQuery,
getClient,
ActionContext,
searchFor
reduceCalls,
removeTxListener,
searchFor,
type ObjectSearchCategory,
type SearchItem
} from '@hcengineering/presentation'
import ui, {
Button,
closePopup,
Component,
Icon,
IconArrowLeft,
Label,
deviceOptionsStore,
capitalizeFirstLetter,
formatKey,
themeStore,
ListView,
resizeObserver
capitalizeFirstLetter,
closePopup,
deviceOptionsStore,
formatKey,
resizeObserver,
themeStore
} from '@hcengineering/ui'
import { Action, ActionCategory, ViewContext } from '@hcengineering/view'
import { createEventDispatcher, onMount, tick } from 'svelte'
import { filterActions, getSelection } from '../actions'
import view from '../plugin'
import { focusStore, selectionStore } from '../selection'
import { openDoc } from '../utils'
import ObjectPresenter from './ObjectPresenter.svelte'
import { createEventDispatcher, tick } from 'svelte'
import { contextStore } from '@hcengineering/presentation'
import ChevronDown from './icons/ChevronDown.svelte'
import ChevronUp from './icons/ChevronUp.svelte'
import { contextStore } from '@hcengineering/presentation'
export let viewContext: ViewContext | undefined = $contextStore.getLastContext()
@ -98,7 +111,7 @@
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)
let fActions: Array<WithLookup<Action>> = actions
@ -125,11 +138,11 @@
fActions = await filterVisibleActions(fActions, docs)
// Sort by category.
supportedActions = fActions.sort((a, b) => a.category.localeCompare(b.category))
}
})
$: 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>> = []
let preparedSearch = search.trim().toLowerCase()
if (preparedSearch.charAt(0) === '/') {
@ -146,7 +159,7 @@
} else {
filteredActions = actions
}
}
})
$: void filterSearchActions(supportedActions, search)
let selection = 0
@ -241,16 +254,32 @@
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[] = []
if (query !== '' && query.indexOf('/') !== 0) {
searchItems = (await searchFor('spotlight', query)).items
}
items = packSearchAndActions(searchItems, 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 phTraslate: string = ''
let autoFocus = !$deviceOptionsStore.isMobile

View File

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

View File

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

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Class, Doc, DocumentQuery, Ref, Space, getCurrentAccount } from '@hcengineering/core'
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 { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view'
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)
for (let i = 0; i < filters.length; i++) {
const filter = filters[i]
@ -141,7 +141,7 @@
}
}
dispatch('change', newQuery)
}
})
$: 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 items = localStorage.getItem(key)
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)
localStorage.setItem(key, JSON.stringify(p))
}
@ -52,7 +52,7 @@
save(_class, p)
})
function onChange (e: Filter | undefined) {
function onChange (e: Filter | undefined): void {
if (e !== undefined) setFilters([e])
}
@ -60,7 +60,7 @@
_class = undefined
})
function add (e: MouseEvent) {
function add (e: MouseEvent): void {
const target = eventToHTMLElement(e)
showPopup(
FilterTypePopup,

View File

@ -18,13 +18,13 @@
Doc,
DocumentQuery,
FindOptions,
RateLimiter,
Ref,
Space,
RateLimiter,
mergeQueries
} from '@hcengineering/core'
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 { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption, Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
@ -70,9 +70,12 @@
$: resultOptions = { ...options, lookup, ...(orderBy !== undefined ? { sort: { [orderBy[0]]: orderBy[1] } } : {}) }
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)
})
$: void update(query, viewOptions)
$: queryNoLookup = noLookup(resultQuery)

View File

@ -26,7 +26,7 @@
Space
} from '@hcengineering/core'
import { getResource, IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { getClient, reduceCalls } from '@hcengineering/presentation'
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
import {
AttributeModel,
@ -100,57 +100,56 @@
CategoryQuery.remove(queryId)
})
function update (): void {
void updateCategories(_class, space, docs, groupByKey, viewOptions, viewOptionsConfig)
}
async function updateCategories (
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
docs: Doc[],
groupByKey: string,
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined
): Promise<void> {
categories = await getCategories(client, _class, space, docs, groupByKey)
if (level === 0) {
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
const f = await getResource(categoryFunc.action)
const res = hierarchy.clone(await f(_class, query, space, groupByKey, update, queryId))
if (res !== undefined) {
categories = concatCategories(res, categories)
return
const updateCategories = reduceCalls(
async (
_class: Ref<Class<Doc>>,
space: Ref<Space> | undefined,
docs: Doc[],
groupByKey: string,
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined
): Promise<void> => {
categories = await getCategories(client, _class, space, docs, groupByKey)
if (level === 0) {
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
const f = await getResource(categoryFunc.action)
const res = hierarchy.clone(await f(_class, query, space, groupByKey, update, queryId))
if (res !== undefined) {
categories = concatCategories(res, categories)
return
}
}
}
}
}
)
function update (): void {
void updateCategories(_class, space, docs, groupByKey, viewOptions, viewOptionsConfig)
}
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) {
headerComponent = undefined
} 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
$: getHeader(_class, groupByKey)
$: void getHeader(_class, groupByKey)
let updateCounter = 0
let configurationsVersion = 0
async function buildModels (
const buildModels = reduceCalls(async function (
_class: Ref<Class<Doc>>,
config: Array<string | BuildModelKey>,
configurations?: Record<Ref<Class<Doc>>, Viewlet['config']> | undefined
): Promise<void> {
const id = ++updateCounter
updateCounter = id
const newItemModels = new Map<Ref<Class<Doc>>, AttributeModel[]>()
const entries = Object.entries(configurations ?? [])
for (const [k, v] of entries) {
@ -164,24 +163,22 @@
newItemModels.set(_class, res)
}
if (id === updateCounter) {
itemModels = newItemModels
configurationsVersion = updateCounter
for (const [, v] of Object.entries(newItemModels)) {
// itemModels = itemModels
;(v as AttributeModel[]).forEach((m: AttributeModel) => {
if (m.displayProps?.key !== undefined) {
const key = `list_item_${m.displayProps.key}`
if (m.displayProps.fixed) {
$fixedWidthStore[key] = 0
}
itemModels = newItemModels
for (const [, v] of Object.entries(newItemModels)) {
// itemModels = itemModels
;(v as AttributeModel[]).forEach((m: AttributeModel) => {
if (m.displayProps?.key !== undefined) {
const key = `list_item_${m.displayProps.key}`
if (m.displayProps.fixed) {
$fixedWidthStore[key] = 0
}
})
}
}
})
}
}
configurationsVersion = configurationsVersion + 1
})
$: buildModels(_class, config, configurations)
$: void buildModels(_class, config, configurations)
function getInitIndex (categories: any, i: number): number {
let res = initIndex

View File

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

View File

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

View File

@ -20,7 +20,14 @@
import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
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 support, { SupportStatus, supportLink } from '@hcengineering/support'
import {
@ -163,18 +170,17 @@
let hasNotificationsFn: ((data: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>) | undefined =
undefined
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?.($inboxNotificationsByContextStore).then((res) => {
$: void hasNotificationsFn?.($inboxNotificationsByContextStore).then((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]) {
// Switch of workspace
return
@ -182,19 +188,15 @@
closeTooltip()
closePopup()
await syncLoc(loc, iteration)
await syncLoc(loc)
await updateWindowTitle(loc)
checkOnHide()
syncPromise = undefined
}
console.log('do sync-end', JSON.stringify(loc), $location.path)
})
onDestroy(
location.subscribe((loc) => {
locUpdate++
if (syncPromise !== undefined) {
void syncPromise.then(() => doSyncLoc(loc, locUpdate))
} else {
syncPromise = doSyncLoc(loc, locUpdate)
}
void doSyncLoc(loc)
})
)
@ -294,15 +296,12 @@
return loc
}
async function syncLoc (loc: Location, iteration: number): Promise<void> {
async function syncLoc (loc: Location): Promise<void> {
const originalLoc = JSON.stringify(loc)
if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) {
// resolve short links
const resolvedLoc = await resolveShortLink(loc)
if (locUpdate !== iteration) {
return
}
if (resolvedLoc !== undefined && !areLocationsEqual(loc, resolvedLoc.loc)) {
loc = mergeLoc(loc, resolvedLoc)
}
@ -334,9 +333,6 @@
let len = 3
if (spaceRef !== undefined && specialRef !== undefined) {
const spaceObj = await client.findOne<Space>(core.class.Space, { _id: spaceRef })
if (locUpdate !== iteration) {
return
}
if (spaceObj !== undefined) {
loc.path[3] = spaceRef
loc.path[4] = specialRef
@ -355,13 +351,7 @@
clear(1)
currentAppAlias = app
currentApplication = await client.findOne<Application>(workbench.class.Application, { alias: app })
if (locUpdate !== iteration) {
return
}
navigatorModel = await buildNavModel(client, currentApplication)
if (locUpdate !== iteration) {
return
}
}
if (
@ -395,9 +385,6 @@
currentSpecial = space
} else {
await updateSpace(space)
if (locUpdate !== iteration) {
return
}
setSpaceSpecial(special)
}
}
@ -425,6 +412,7 @@
if (props.length >= 3) {
const doc = await client.findOne<Doc>(props[2] as Ref<Class<Doc>>, { _id: props[1] as Ref<Doc> })
if (doc !== undefined) {
const provider = ListSelectionProvider.Find(doc._id)
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 {
const initWS = getMetadata(toolPlugin.metadata.InitWorkspace)
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
await (
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>()
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)
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.attachedToClass != null && 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 states = await migrateClient.find<MigrationState>(DOMAIN_MIGRATION, { _class: core.class.MigrationState })
const migrateState = new Map(
Array.from(groupByArray(states, (it) => it.plugin).entries()).map((it) => [
it[0],
new Set(it[1].map((q) => q.state))
])
)
const sts = Array.from(groupByArray(states, (it) => it.plugin).entries())
const migrateState = new Map(sts.map((it) => [it[0], new Set(it[1].map((q) => q.state))]))
migrateClient.migrateState = migrateState
await ctx.with('migrate', {}, async () => {
let i = 0
for (const op of migrateOperations) {
const t = Date.now()
await op[1].migrate(migrateClient, logger)
try {
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 })
await progress(20 + ((100 / migrateOperations.length) * i * 20) / 100)
i++

View File

@ -19,73 +19,78 @@ test.describe('Collaborative tests for Application', () => {
const vacancyName = 'Software Engineer'
let talentName: TalentName
// open second page
const userSecondPage = await getSecondPage(browser)
const { page: userSecondPage, context } = await getSecondPage(browser)
await test.step('User1. Add collaborators and comment from user1', async () => {
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonApplications.click()
try {
await test.step('User1. Add collaborators and comment from user1', async () => {
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonApplications.click()
const applicationsPage = new ApplicationsPage(page)
talentName = await applicationsPage.createNewApplicationWithNewTalent({
vacancy: vacancyName,
recruiterName: 'first'
const applicationsPage = new ApplicationsPage(page)
talentName = await applicationsPage.createNewApplicationWithNewTalent({
vacancy: vacancyName,
recruiterName: 'first'
})
await applicationsPage.openApplicationByTalentName(talentName)
const applicationsDetailsPage = new ApplicationsDetailsPage(page)
await applicationsDetailsPage.addCollaborators('Dirak Kainin')
await applicationsDetailsPage.addComment('Test Comment from user1')
await applicationsDetailsPage.checkCommentExist('Test Comment from user1')
})
await applicationsPage.openApplicationByTalentName(talentName)
const applicationsDetailsPage = new ApplicationsDetailsPage(page)
await applicationsDetailsPage.addCollaborators('Dirak Kainin')
await applicationsDetailsPage.addComment('Test Comment from user1')
await applicationsDetailsPage.checkCommentExist('Test Comment from user1')
})
await test.step('User2. Check notification and add comment from user2', async () => {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/recruit`))?.finished()
await test.step('User2. Check notification and add comment from user2', async () => {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/recruit`))?.finished()
const leftSideMenuPageSecond = new LeftSideMenuPage(userSecondPage)
await leftSideMenuPageSecond.checkExistNewNotification(userSecondPage)
await leftSideMenuPageSecond.buttonNotification.click()
const leftSideMenuPageSecond = new LeftSideMenuPage(userSecondPage)
await leftSideMenuPageSecond.checkExistNewNotification(userSecondPage)
await leftSideMenuPageSecond.buttonNotification.click()
// TODO: rewrite checkNotificationCollaborators and uncomment
// const notificationPageSecond = new NotificationPage(userSecondPage)
// await notificationPageSecond.checkNotificationCollaborators(
// `${talentName.lastName} ${talentName.firstName}`,
// 'You have been added to collaborators'
// )
// TODO: rewrite checkNotificationCollaborators and uncomment
// const notificationPageSecond = new NotificationPage(userSecondPage)
// await notificationPageSecond.checkNotificationCollaborators(
// `${talentName.lastName} ${talentName.firstName}`,
// 'You have been added to collaborators'
// )
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/recruit`))?.finished()
const navigationMenuPageSecond = new NavigationMenuPage(userSecondPage)
await navigationMenuPageSecond.buttonApplications.click()
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/recruit`))?.finished()
const navigationMenuPageSecond = new NavigationMenuPage(userSecondPage)
await navigationMenuPageSecond.buttonApplications.click()
const applicationsPageSecond = new ApplicationsPage(userSecondPage)
await applicationsPageSecond.openApplicationByTalentName(talentName)
const applicationsPageSecond = new ApplicationsPage(userSecondPage)
await applicationsPageSecond.openApplicationByTalentName(talentName)
const applicationsDetailsPageSecond = new ApplicationsDetailsPage(userSecondPage)
await applicationsDetailsPageSecond.checkCommentExist('Test Comment from user1')
await applicationsDetailsPageSecond.addComment('Test Comment from user2')
await applicationsDetailsPageSecond.checkCommentExist('Test Comment from user2')
})
const applicationsDetailsPageSecond = new ApplicationsDetailsPage(userSecondPage)
await applicationsDetailsPageSecond.checkCommentExist('Test Comment from user1')
await applicationsDetailsPageSecond.addComment('Test Comment from user2')
await applicationsDetailsPageSecond.checkCommentExist('Test Comment from user2')
})
await test.step('User1. Check notification and check comment from user1', async () => {
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.checkExistNewNotification(page)
await leftSideMenuPage.buttonNotification.click()
await test.step('User1. Check notification and check comment from user1', async () => {
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.checkExistNewNotification(page)
await leftSideMenuPage.buttonNotification.click()
// TODO: rewrite checkNotificationCollaborators and uncomment
// const notificationPage = new NotificationPage(page)
// await notificationPage.checkNotificationCollaborators(
// `${talentName.lastName} ${talentName.firstName}`,
// 'left a comment'
// )
// TODO: rewrite checkNotificationCollaborators and uncomment
// const notificationPage = new NotificationPage(page)
// await notificationPage.checkNotificationCollaborators(
// `${talentName.lastName} ${talentName.firstName}`,
// 'left a comment'
// )
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/recruit`))?.finished()
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonApplications.click()
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/recruit`))?.finished()
const navigationMenuPage = new NavigationMenuPage(page)
await navigationMenuPage.buttonApplications.click()
const applicationsPage = new ApplicationsPage(page)
await applicationsPage.openApplicationByTalentName(talentName)
const applicationsPage = new ApplicationsPage(page)
await applicationsPage.openApplicationByTalentName(talentName)
const applicationsDetailsPage = new ApplicationsDetailsPage(page)
await applicationsDetailsPage.checkCommentExist('Test Comment from user2')
})
const applicationsDetailsPage = new ApplicationsDetailsPage(page)
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 { 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 { 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({
storageState: PlatformSetting
@ -22,7 +22,7 @@ test.describe('Collaborative test for issue', () => {
priority: 'Urgent',
assignee: 'Appleseed John',
createLabel: true,
labels: `CREATE-ISSUE-${generateId()}`,
labels: `CREATE-ISSUE-${generateId(5)}`,
component: 'No component',
estimation: '2',
milestone: 'No Milestone',
@ -31,38 +31,42 @@ test.describe('Collaborative test for issue', () => {
}
// open second page
const userSecondPage = await getSecondPage(browser)
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const { page: userSecondPage, context } = await getSecondPage(browser)
try {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
// create a new issue by first user
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonTracker.click()
// create a new issue by first user
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonTracker.click()
const issuesPage = new IssuesPage(page)
await issuesPage.createNewIssue(newIssue)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorAll.click()
await issuesPage.searchIssueByName(newIssue.title)
await issuesPage.openIssueByName(newIssue.title)
const issuesPage = new IssuesPage(page)
await issuesPage.createNewIssue(newIssue)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorAll.click()
await issuesPage.searchIssueByName(newIssue.title)
await issuesPage.openIssueByName(newIssue.title)
// check created issued by second user
const issuesPageSecond = new IssuesPage(userSecondPage)
await userSecondPage.evaluate(() => {
localStorage.setItem('platform.activity.threshold', '0')
})
await issuesPageSecond.linkSidebarAll.click()
await issuesPageSecond.modelSelectorAll.click()
await issuesPageSecond.searchIssueByName(newIssue.title)
await issuesPageSecond.openIssueByName(newIssue.title)
// check created issued by second user
const issuesPageSecond = new IssuesPage(userSecondPage)
await userSecondPage.evaluate(() => {
localStorage.setItem('platform.activity.threshold', '0')
})
await issuesPageSecond.linkSidebarAll.click()
await issuesPageSecond.modelSelectorAll.click()
await issuesPageSecond.searchIssueByName(newIssue.title)
await issuesPageSecond.openIssueByName(newIssue.title)
const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage)
await issuesDetailsPageSecond.checkIssue({
...newIssue,
milestone: 'Milestone',
estimation: '2h'
})
await userSecondPage.close()
const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage)
await issuesDetailsPageSecond.checkIssue({
...newIssue,
milestone: 'Milestone',
estimation: '2h'
})
} finally {
await userSecondPage.close()
await context.close()
}
})
test('Issues status can be changed by another users', async ({ page, browser }) => {
@ -72,42 +76,48 @@ test.describe('Collaborative test for issue', () => {
}
// open second page
const userSecondPage = await getSecondPage(browser)
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const { page: userSecondPage, context } = await getSecondPage(browser)
const issuesPageSecond = new IssuesPage(userSecondPage)
await issuesPageSecond.linkSidebarAll.click()
await issuesPageSecond.modelSelectorAll.click()
try {
if (userSecondPage.url() !== `${PlatformURI}/workbench/sanity-ws/tracker/`) {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
}
// change status
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const issuesPage = new IssuesPage(page)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorBacklog.click()
await issuesPage.searchIssueByName(issue.title)
await issuesPage.openIssueByName(issue.title)
const issuesPageSecond = new IssuesPage(userSecondPage)
await issuesPageSecond.linkSidebarAll.click()
await issuesPageSecond.modelSelectorAll.click()
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.editIssue({ status: 'In Progress' })
// change status
const issuesPage = new IssuesPage(page)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorBacklog.click()
await issuesPage.searchIssueByName(issue.title)
await issuesPage.openIssueByName(issue.title)
// check by another user
await issuesPageSecond.modelSelectorBacklog.click()
// not active for another user
await issuesPageSecond.checkIssueNotExist(issue.title)
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.editIssue({ status: 'In Progress' })
await issuesPageSecond.modelSelectorActive.click()
await issuesPageSecond.searchIssueByName(issue.title)
await issuesPageSecond.openIssueByName(issue.title)
// check by another user
await issuesPageSecond.modelSelectorBacklog.click()
// not active for another user
await issuesPageSecond.checkIssueNotExist(issue.title)
const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage)
await userSecondPage.evaluate(() => {
localStorage.setItem('platform.activity.threshold', '0')
})
await issuesDetailsPageSecond.checkIssue({
...issue,
status: 'In Progress'
})
await userSecondPage.close()
await issuesPageSecond.modelSelectorActive.click()
await issuesPageSecond.searchIssueByName(issue.title)
await issuesPageSecond.openIssueByName(issue.title)
const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage)
await userSecondPage.evaluate(() => {
localStorage.setItem('platform.activity.threshold', '0')
})
await issuesDetailsPageSecond.checkIssue({
...issue,
status: 'In Progress'
})
} finally {
await userSecondPage.close()
await context.close()
}
})
test('First user change assignee, second user should see assigned issue', async ({ page, browser }) => {
@ -118,44 +128,48 @@ test.describe('Collaborative test for issue', () => {
}
// open second page
const userSecondPage = await getSecondPage(browser)
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const { page: userSecondPage, context } = await getSecondPage(browser)
try {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
await test.step(`user1. change assignee to ${newAssignee}`, async () => {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const issuesPage = new IssuesPage(page)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorBacklog.click()
await issuesPage.searchIssueByName(issue.title)
await issuesPage.openIssueByName(issue.title)
await test.step(`user1. change assignee to ${newAssignee}`, async () => {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/tracker/`))?.finished()
const issuesPage = new IssuesPage(page)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorBacklog.click()
await issuesPage.searchIssueByName(issue.title)
await issuesPage.openIssueByName(issue.title)
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.editIssue({ assignee: newAssignee })
})
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.editIssue({ assignee: newAssignee })
})
// TODO: rewrite checkNotificationIssue and uncomment
// await test.step('user2. check notification', async () => {
// const leftSideMenuPageSecond = new LeftSideMenuPage(userSecondPage)
// await leftSideMenuPageSecond.checkExistNewNotification(userSecondPage)
// await leftSideMenuPageSecond.buttonNotification.click()
//
// const notificationPageSecond = new NotificationPage(userSecondPage)
// await notificationPageSecond.checkNotificationIssue(issue.title, newAssignee)
//
// await leftSideMenuPageSecond.buttonTracker.click()
// })
// TODO: rewrite checkNotificationIssue and uncomment
// await test.step('user2. check notification', async () => {
// const leftSideMenuPageSecond = new LeftSideMenuPage(userSecondPage)
// await leftSideMenuPageSecond.checkExistNewNotification(userSecondPage)
// await leftSideMenuPageSecond.buttonNotification.click()
//
// const notificationPageSecond = new NotificationPage(userSecondPage)
// await notificationPageSecond.checkNotificationIssue(issue.title, newAssignee)
//
// await leftSideMenuPageSecond.buttonTracker.click()
// })
await test.step('user2. check issue assignee', async () => {
const issuesPageSecond = new IssuesPage(userSecondPage)
await issuesPageSecond.linkSidebarMyIssue.click()
await issuesPageSecond.modelSelectorBacklog.click()
await test.step('user2. check issue assignee', async () => {
const issuesPageSecond = new IssuesPage(userSecondPage)
await issuesPageSecond.linkSidebarMyIssue.click()
await issuesPageSecond.modelSelectorBacklog.click()
await issuesPageSecond.searchIssueByName(issue.title)
await issuesPageSecond.openIssueByName(issue.title)
await issuesPageSecond.searchIssueByName(issue.title)
await issuesPageSecond.openIssueByName(issue.title)
const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage)
await issuesDetailsPageSecond.checkIssue({ ...issue })
})
await userSecondPage.close()
const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage)
await issuesDetailsPageSecond.checkIssue({ ...issue })
})
} finally {
await userSecondPage.close()
await context.close()
}
})
})

View File

@ -1,10 +1,10 @@
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 { 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 { generateId, PlatformSetting, PlatformURI } from '../utils'
test.describe('Documents link tests', () => {
test('Document public link revoke', async ({ browser }) => {
@ -15,46 +15,56 @@ test.describe('Documents link tests', () => {
const newContext = await browser.newContext({ storageState: PlatformSetting })
const page = await newContext.newPage()
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
try {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonDocuments.click()
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonDocuments.click()
const documentsPage = new DocumentsPage(page)
await documentsPage.buttonCreateDocument.click()
const documentsPage = new DocumentsPage(page)
await documentsPage.buttonCreateDocument.click()
await documentsPage.createDocument(publicLinkDocument)
await documentsPage.openDocument(publicLinkDocument.title)
await documentsPage.createDocument(publicLinkDocument)
await documentsPage.openDocument(publicLinkDocument.title)
const documentContentPage = new DocumentContentPage(page)
await documentContentPage.executeMoreAction('Public link')
const documentContentPage = new DocumentContentPage(page)
await documentContentPage.executeMoreAction('Public link')
// remove after UBERF-5994 fixed
await documentContentPage.closePopup(page)
await page.reload({ waitUntil: 'commit' })
await documentContentPage.executeMoreAction('Public link')
// remove after UBERF-5994 fixed
await documentContentPage.closePopup(page)
await page.reload({ waitUntil: 'commit' })
await documentContentPage.executeMoreAction('Public link')
const publicLinkPopup = new PublicLinkPopup(page)
const link = await publicLinkPopup.getPublicLink()
const clearSession = await browser.newContext()
const clearPage = await clearSession.newPage()
await test.step('Check guest access to the document', async () => {
await clearPage.goto(link)
const documentContentClearPage = new DocumentContentPage(clearPage)
await documentContentClearPage.checkDocumentTitle(publicLinkDocument.title)
expect(clearPage.url()).toContain('guest')
})
await test.step('Revoke guest access to the document', async () => {
const publicLinkPopup = new PublicLinkPopup(page)
await publicLinkPopup.revokePublicLink()
})
const link = await publicLinkPopup.getPublicLink()
await test.step('Check guest access to the document after the revoke', async () => {
await clearPage.goto(link)
await expect(clearPage.locator('div.antiPopup > h1')).toHaveText('Public link was revoked')
})
const clearSession = await browser.newContext()
const clearPage = await clearSession.newPage()
try {
await test.step('Check guest access to the document', async () => {
await clearPage.goto(link)
const documentContentClearPage = new DocumentContentPage(clearPage)
await documentContentClearPage.checkDocumentTitle(publicLinkDocument.title)
expect(clearPage.url()).toContain('guest')
})
await test.step('Revoke guest access to the document', async () => {
const publicLinkPopup = new PublicLinkPopup(page)
await publicLinkPopup.revokePublicLink()
})
await test.step('Check guest access to the document after the revoke', async () => {
await clearPage.goto(link)
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,22 +130,27 @@ test.describe('Documents tests', () => {
})
await test.step('User2. Add content second user', async () => {
const userSecondPage = await getSecondPage(browser)
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
const { page: userSecondPage, context } = await getSecondPage(browser)
try {
await (await userSecondPage.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
const leftSideMenuPageSecond = new LeftSideMenuPage(userSecondPage)
await leftSideMenuPageSecond.buttonDocuments.click()
const leftSideMenuPageSecond = new LeftSideMenuPage(userSecondPage)
await leftSideMenuPageSecond.buttonDocuments.click()
const documentsPageSecond = new DocumentsPage(userSecondPage)
await documentsPageSecond.openTeamspace(colDocument.space)
await documentsPageSecond.openDocument(colDocument.title)
const documentsPageSecond = new DocumentsPage(userSecondPage)
await documentsPageSecond.openTeamspace(colDocument.space)
await documentsPageSecond.openDocument(colDocument.title)
const documentContentPageSecond = new DocumentContentPage(page)
await documentContentPageSecond.checkDocumentTitle(colDocument.title)
await documentContentPageSecond.checkContent(content)
const documentContentPageSecond = new DocumentContentPage(page)
await documentContentPageSecond.checkDocumentTitle(colDocument.title)
await documentContentPageSecond.checkContent(content)
content = await documentContentPageSecond.addContentToTheNewLine(contentSecondUser)
await documentContentPageSecond.checkContent(content)
content = await documentContentPageSecond.addContentToTheNewLine(contentSecondUser)
await documentContentPageSecond.checkContent(content)
} finally {
await userSecondPage.close()
await context.close()
}
})
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"]')
.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"]').waitFor({ state: 'hidden' })
}
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> {
await expect(this.inputContent).toBeVisible()
await expect(this.inputContent).toHaveJSProperty('contentEditable', 'true')
await this.inputContent.pressSequentially(`\n${newContent}`)
const endContent = await this.inputContent.textContent()
if (endContent == null) {
@ -42,6 +43,7 @@ export class DocumentContentPage extends CommonPage {
async updateDocumentTitle (title: string): Promise<void> {
await this.buttonDocumentTitle.fill(title)
await this.buttonDocumentTitle.blur()
}
async addRandomLines (count: number, lineLength: number = 36): Promise<void> {

View File

@ -117,9 +117,10 @@ export class PlanningPage extends CalendarPage {
if (data.createLabel) {
await this.pressCreateButtonSelectPopup(this.page)
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) {
let index = 0

View File

@ -31,7 +31,7 @@ export class IssuesDetailsPage extends CommonTrackerPage {
super(page)
this.page = page
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.buttonPriority = page.locator('//span[text()="Priority"]/../button[2]//span')
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> {
const existDescription = await this.inputDescription.textContent()
await expect(this.inputDescription).toHaveJSProperty('contentEditable', 'true')
await this.inputDescription.fill(`${existDescription}\n${description}`)
}

View File

@ -110,11 +110,10 @@ export class IssuesPage extends CommonTrackerPage {
if (data.createLabel) {
await this.pressCreateButtonSelectPopup(this.page)
await this.addNewTagPopup(this.page, data.labels, 'Tag from createNewIssue')
await this.page.locator('.popup#TagsPopup').press('Escape')
} else {
await this.checkFromDropdown(this.page, data.labels)
}
await this.inputPopupCreateNewIssueTitle.press('Escape')
await this.inputPopupCreateNewIssueTitle.click({ force: true })
}
if (data.component != null) {
await this.buttonPopupCreateNewIssueComponent.click()
@ -211,10 +210,14 @@ export class IssuesPage extends CommonTrackerPage {
}
async checkAllIssuesByPriority (priorityName: string): Promise<void> {
for await (const locator of iterateLocator(this.issuesList)) {
const href = await locator.locator('div.priority-container use').getAttribute('href')
expect(href).toContain(priorityName)
}
await expect(async () => {
for await (const locator of iterateLocator(this.issuesList)) {
const href = await locator.locator('div.priority-container use').getAttribute('href')
expect(href, { message: `Should contain ${priorityName} but it is ${href}` }).toContain(priorityName)
}
}).toPass({
timeout: 15000
})
}
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> {
return await test.step('Prepare document', async () => {
const issuesPage = new IssuesPage(page)
await issuesPage.linkSidebarAll.click()
await issuesPage.modelSelectorAll.click()
await issuesPage.createNewIssue(issue)
await issuesPage.searchIssueByName(issue.title)
await issuesPage.openIssueByName(issue.title)

View File

@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test'
import { navigate } from './tracker.utils'
import { generateId, PlatformSetting, PlatformURI, fillSearch } from '../utils'
import { LeftSideMenuPage } from '../model/left-side-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 }) => {
await page.click('[id="app-tracker\\:string\\:TrackerApplication"]')
await navigate(page)
await page.click('text=Components')
await expect(page).toHaveURL(
`${PlatformURI}/workbench/sanity-ws/tracker/tracker%3Aproject%3ADefaultProject/components`

View File

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

View File

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

View File

@ -17,17 +17,22 @@ test.describe('Tracker public link issues tests', () => {
await test.step('Get public link from popup', async () => {
const newContext = await browser.newContext({ storageState: PlatformSetting })
const page = await newContext.newPage()
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
try {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonTracker.click()
await prepareNewIssueWithOpenStep(page, publicLinkIssue)
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonTracker.click()
await prepareNewIssueWithOpenStep(page, publicLinkIssue)
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.moreActionOnIssue('Public link')
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.moreActionOnIssue('Public link')
const publicLinkPopup = new PublicLinkPopup(page)
link = await publicLinkPopup.getPublicLink()
const publicLinkPopup = new PublicLinkPopup(page)
link = await publicLinkPopup.getPublicLink()
} finally {
await page.close()
await newContext.close()
}
})
await test.step('Check guest access to the issue', async () => {
@ -36,15 +41,20 @@ test.describe('Tracker public link issues tests', () => {
await clearSession.clearPermissions()
const clearPage = await clearSession.newPage()
await clearPage.goto(link)
try {
await clearPage.goto(link)
const clearIssuesDetailsPage = new IssuesDetailsPage(clearPage)
await clearIssuesDetailsPage.waitDetailsOpened(publicLinkIssue.title)
await clearIssuesDetailsPage.checkIssue({
...publicLinkIssue,
status: 'Backlog'
})
expect(clearPage.url()).toContain('guest')
const clearIssuesDetailsPage = new IssuesDetailsPage(clearPage)
await clearIssuesDetailsPage.waitDetailsOpened(publicLinkIssue.title)
await clearIssuesDetailsPage.checkIssue({
...publicLinkIssue,
status: 'Backlog'
})
expect(clearPage.url()).toContain('guest')
} finally {
await clearPage.close()
await clearSession.close()
}
})
})
@ -57,42 +67,52 @@ test.describe('Tracker public link issues tests', () => {
const newContext = await browser.newContext({ storageState: PlatformSetting })
const page = await newContext.newPage()
let link: string
await test.step('Get public link from popup', async () => {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
try {
await test.step('Get public link from popup', async () => {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonTracker.click()
await prepareNewIssueWithOpenStep(page, publicLinkIssue)
const leftSideMenuPage = new LeftSideMenuPage(page)
await leftSideMenuPage.buttonTracker.click()
await prepareNewIssueWithOpenStep(page, publicLinkIssue)
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.moreActionOnIssue('Public link')
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.moreActionOnIssue('Public link')
const publicLinkPopup = new PublicLinkPopup(page)
link = await publicLinkPopup.getPublicLink()
})
const clearSession = await browser.newContext()
const clearPage = await clearSession.newPage()
await test.step('Check guest access to the issue', async () => {
await clearPage.goto(link)
const clearIssuesDetailsPage = new IssuesDetailsPage(clearPage)
await clearIssuesDetailsPage.waitDetailsOpened(publicLinkIssue.title)
await clearIssuesDetailsPage.checkIssue({
...publicLinkIssue,
status: 'Backlog'
const publicLinkPopup = new PublicLinkPopup(page)
link = await publicLinkPopup.getPublicLink()
})
expect(clearPage.url()).toContain('guest')
})
await test.step('Revoke guest access to the issue', async () => {
const publicLinkPopup = new PublicLinkPopup(page)
await publicLinkPopup.revokePublicLink()
})
const clearSession = await browser.newContext()
const clearPage = await clearSession.newPage()
try {
await test.step('Check guest access to the issue', async () => {
await clearPage.goto(link)
await test.step('Check guest access to the issue after the revoke', async () => {
await clearPage.goto(link)
await expect(clearPage.locator('div.antiPopup > h1')).toHaveText('Public link was revoked')
})
const clearIssuesDetailsPage = new IssuesDetailsPage(clearPage)
await clearIssuesDetailsPage.waitDetailsOpened(publicLinkIssue.title)
await clearIssuesDetailsPage.checkIssue({
...publicLinkIssue,
status: 'Backlog'
})
expect(clearPage.url()).toContain('guest')
})
await test.step('Revoke guest access to the issue', async () => {
const publicLinkPopup = new PublicLinkPopup(page)
await publicLinkPopup.revokePublicLink()
})
await test.step('Check guest access to the issue after the revoke', async () => {
await clearPage.goto(link)
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 }) => {
await navigate(page)
await page.click('[id="app-tracker\\:string\\:TrackerApplication"]')
const props = {
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'
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
}
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 })
return await userSecondContext.newPage()
return { page: await userSecondContext.newPage(), context: userSecondContext }
}
export function expectToContainsOrdered (val: Locator, text: string[], timeout?: number): Promise<void> {
const origIssuesExp = new RegExp('.*' + text.join('.*') + '.*')