mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 01:10:17 +00:00
UBERF-6330: Fix race conditions in UI (#5184)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
d3fa4908ef
commit
0d1d1a8b8d
@ -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) => {}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -43,5 +43,6 @@
|
||||
close={popup.close}
|
||||
{contentPanel}
|
||||
overlay={popup.options.overlay}
|
||||
{popup}
|
||||
/>
|
||||
{/each}
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: updateFilterActions(
|
||||
$: void updateFilterActions(
|
||||
messages,
|
||||
filters,
|
||||
selectedFiltersRefs,
|
||||
|
@ -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) } },
|
||||
|
@ -43,7 +43,7 @@
|
||||
|
||||
$: updateResultQuery(search)
|
||||
|
||||
function showCreateDialog () {
|
||||
function showCreateDialog (): void {
|
||||
if (createComponent === undefined) {
|
||||
return
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)) {
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
})
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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(', ')
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -84,7 +84,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
id={'new-document'}
|
||||
id={'new-teamspace'}
|
||||
icon={IconAdd}
|
||||
label={document.string.CreateTeamspace}
|
||||
justify={'left'}
|
||||
|
@ -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)
|
||||
|
@ -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> {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -73,6 +73,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
refId: 'TagsPopup',
|
||||
category: 'popup',
|
||||
overlay: true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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">
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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) } },
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
|
@ -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
|
||||
|
@ -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 = ''
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 })
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
@ -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>
|
@ -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>
|
@ -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) => {
|
||||
|
@ -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)) {
|
||||
|
@ -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++
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -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 () => {
|
||||
|
@ -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> {
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
|
@ -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}`)
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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)
|
||||
|
@ -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`
|
||||
|
@ -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) {
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -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)}`,
|
||||
|
@ -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('.*') + '.*')
|
||||
|
Loading…
Reference in New Issue
Block a user