UBERF-8993: Part2 (#7532)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-12-24 17:39:51 +07:00 committed by GitHub
parent 0db7adccd6
commit 35981be705
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 178 additions and 114 deletions

1
.gitignore vendored
View File

@ -106,3 +106,4 @@ tests/profiles
dump dump
**/logs/** **/logs/**
dev/tool/history.json dev/tool/history.json
.aider*

View File

@ -28,22 +28,23 @@ function $push (document: Doc, keyval: Record<string, PropertyType>): void {
if (doc[key] === undefined) { if (doc[key] === undefined) {
doc[key] = [] doc[key] = []
} }
const val = keyval[key] const kvk = keyval[key]
if (typeof val === 'object') { if (typeof kvk === 'object' && kvk != null) {
const arr = doc[key] as Array<any> const arr = doc[key] as Array<any>
const desc = val as Position<PropertyType> const desc = kvk as Position<PropertyType>
if ('$each' in desc) { if ('$each' in desc) {
if (arr != null) { if (arr != null && Array.isArray(arr)) {
arr.splice(desc.$position ?? 0, 0, ...desc.$each) arr.splice(desc.$position ?? 0, 0, ...desc.$each)
} }
} else { } else {
arr.push(val) arr.push(kvk)
} }
} else { } else {
if (doc[key] == null) { if (doc[key] === null || doc[key] === undefined) {
doc[key] = [] doc[key] = [kvk]
} else {
doc[key].push(kvk)
} }
doc[key].push(val)
} }
} }
} }
@ -55,15 +56,16 @@ function $pull (document: Doc, keyval: Record<string, PropertyType>): void {
doc[key] = [] doc[key] = []
} }
const arr = doc[key] as Array<any> const arr = doc[key] as Array<any>
if (typeof keyval[key] === 'object' && keyval[key] !== null) { const kvk = keyval[key]
const { $in } = keyval[key] as PullArray<PropertyType> if (typeof kvk === 'object' && kvk !== null) {
const { $in } = kvk as PullArray<PropertyType>
doc[key] = (arr ?? []).filter((val) => { doc[key] = (arr ?? []).filter((val) => {
if ($in !== undefined) { if ($in !== undefined) {
return !$in.includes(val) return !$in.includes(val)
} else { } else {
// We need to match all fields // We need to match all fields
for (const [kk, kv] of Object.entries(keyval[key])) { for (const [kk, kv] of Object.entries(kvk)) {
if (val[kk] !== kv) { if (val[kk] !== kv) {
return true return true
} }
@ -72,7 +74,7 @@ function $pull (document: Doc, keyval: Record<string, PropertyType>): void {
} }
}) })
} else { } else {
doc[key] = (arr ?? []).filter((val) => val !== keyval[key]) doc[key] = (arr ?? []).filter((val) => val !== kvk)
} }
} }
} }

View File

@ -93,13 +93,14 @@
dispatch('update', selectedObjects) dispatch('update', selectedObjects)
} }
const client = getClient()
let selection = 0 let selection = 0
let list: ListView let list: ListView
async function handleSelection (evt: Event | undefined, objects: Doc[], selection: number): Promise<void> { async function handleSelection (evt: Event | undefined, objects: Doc[], selection: number): Promise<void> {
const item = objects[selection] const item = objects[selection]
if (item === undefined) {
return
}
if (!multiSelect) { if (!multiSelect) {
if (allowDeselect) { if (allowDeselect) {
@ -140,7 +141,7 @@
showPopup(c.component, c.props ?? {}, 'top', async (res) => { showPopup(c.component, c.props ?? {}, 'top', async (res) => {
if (res != null) { if (res != null) {
// We expect reference to new object. // We expect reference to new object.
const newPerson = await client.findOne(_class, { _id: res }) const newPerson = await getClient().findOne(_class, { _id: res })
if (newPerson !== undefined) { if (newPerson !== undefined) {
search = c.update?.(newPerson) ?? '' search = c.update?.(newPerson) ?? ''
dispatch('created', newPerson) dispatch('created', newPerson)
@ -163,7 +164,7 @@
} }
function findObjectPresenter (_class: Ref<Class<Doc>>): void { function findObjectPresenter (_class: Ref<Class<Doc>>): void {
const presenterMixin = client.getHierarchy().classHierarchyMixin(_class, view.mixin.ObjectPresenter) const presenterMixin = getClient().getHierarchy().classHierarchyMixin(_class, view.mixin.ObjectPresenter)
if (presenterMixin?.presenter !== undefined) { if (presenterMixin?.presenter !== undefined) {
getResource(presenterMixin.presenter) getResource(presenterMixin.presenter)
.then((result) => { .then((result) => {

View File

@ -46,7 +46,6 @@
let categories: ObjectSearchCategory[] = [] let categories: ObjectSearchCategory[] = []
let categoryStatus: Record<Ref<ObjectSearchCategory>, number> = {} let categoryStatus: Record<Ref<ObjectSearchCategory>, number> = {}
const client = getClient()
let category: ObjectSearchCategory | undefined let category: ObjectSearchCategory | undefined
const categoryQuery: DocumentQuery<ObjectSearchCategory> = { const categoryQuery: DocumentQuery<ObjectSearchCategory> = {
@ -56,10 +55,12 @@
categoryQuery._id = { $in: allowCategory } categoryQuery._id = { $in: allowCategory }
} }
client.findAll(presentation.class.ObjectSearchCategory, categoryQuery).then((r) => { void getClient()
categories = r.filter((it) => hasResource(it.query)) .findAll(presentation.class.ObjectSearchCategory, categoryQuery)
category = categories[0] .then((r) => {
}) categories = r.filter((it) => hasResource(it.query))
category = categories[0]
})
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -96,7 +97,7 @@
key.preventDefault() key.preventDefault()
key.stopPropagation() key.stopPropagation()
const item = items[selection] const item = items[selection]
if (item) { if (item != null) {
dispatchItem(item) dispatchItem(item)
return true return true
} else { } else {
@ -106,7 +107,7 @@
return false return false
} }
export function done () {} export function done (): void {}
const updateItems = reduceCalls(async function updateItems ( const updateItems = reduceCalls(async function updateItems (
cat: ObjectSearchCategory | undefined, cat: ObjectSearchCategory | undefined,
@ -120,6 +121,7 @@
const newCategoryStatus: Record<Ref<ObjectSearchCategory>, number> = {} const newCategoryStatus: Record<Ref<ObjectSearchCategory>, number> = {}
const f = await getResource(cat.query) const f = await getResource(cat.query)
const client = getClient()
const result = await f(client, query, { in: relatedDocuments, nin: ignore }) const result = await f(client, query, { in: relatedDocuments, nin: ignore })
// We need to sure, results we return is for proper category. // We need to sure, results we return is for proper category.
if (cat._id === category?._id) { if (cat._id === category?._id) {

View File

@ -53,7 +53,7 @@ import { getRawCurrentLocation, workspaceId, type AnyComponent, type AnySvelteCo
import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view' import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import { get, writable, type Writable } from 'svelte/store' import { get, writable } from 'svelte/store'
import { type KeyedAttribute } from '..' import { type KeyedAttribute } from '..'
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline' import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
@ -63,7 +63,7 @@ export { reduceCalls } from '@hcengineering/core'
let liveQuery: LQ let liveQuery: LQ
let rawLiveQuery: LQ let rawLiveQuery: LQ
let client: TxOperations & Client & OptimisticTxes let client: TxOperations & Client
let pipeline: PresentationPipeline let pipeline: PresentationPipeline
const txListeners: Array<(...tx: Tx[]) => void> = [] const txListeners: Array<(...tx: Tx[]) => void> = []
@ -89,13 +89,11 @@ export function removeTxListener (l: (tx: Tx) => void): void {
} }
} }
export interface OptimisticTxes {
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
}
export const uiContext = new MeasureMetricsContext('client-ui', {}) export const uiContext = new MeasureMetricsContext('client-ui', {})
class UIClient extends TxOperations implements Client, OptimisticTxes { export const pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
class UIClient extends TxOperations implements Client {
hook = getMetadata(plugin.metadata.ClientHook) hook = getMetadata(plugin.metadata.ClientHook)
constructor ( constructor (
client: Client, client: Client,
@ -105,14 +103,9 @@ class UIClient extends TxOperations implements Client, OptimisticTxes {
} }
protected pendingTxes = new Set<Ref<Tx>>() protected pendingTxes = new Set<Ref<Tx>>()
protected _pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
get pendingCreatedDocs (): typeof this._pendingCreatedDocs {
return this._pendingCreatedDocs
}
async doNotify (...tx: Tx[]): Promise<void> { async doNotify (...tx: Tx[]): Promise<void> {
const pending = get(this._pendingCreatedDocs) const pending = get(pendingCreatedDocs)
let pendingUpdated = false let pendingUpdated = false
tx.forEach((t) => { tx.forEach((t) => {
if (this.pendingTxes.has(t._id)) { if (this.pendingTxes.has(t._id)) {
@ -129,7 +122,7 @@ class UIClient extends TxOperations implements Client, OptimisticTxes {
} }
}) })
if (pendingUpdated) { if (pendingUpdated) {
this._pendingCreatedDocs.set(pending) pendingCreatedDocs.set(pending)
} }
// We still want to notify about all transactions because there might be queries created after // We still want to notify about all transactions because there might be queries created after
@ -214,9 +207,9 @@ class UIClient extends TxOperations implements Client, OptimisticTxes {
} }
if (innerTx._class === core.class.TxCreateDoc) { if (innerTx._class === core.class.TxCreateDoc) {
const pending = get(this._pendingCreatedDocs) const pending = get(pendingCreatedDocs)
pending[innerTx.objectId] = true pending[innerTx.objectId] = true
this._pendingCreatedDocs.set(pending) pendingCreatedDocs.set(pending)
} }
this.pendingTxes.add(tx._id) this.pendingTxes.add(tx._id)
@ -231,11 +224,33 @@ class UIClient extends TxOperations implements Client, OptimisticTxes {
} }
} }
const hierarchyProxy = new Proxy(
{},
{
get (target, p, receiver) {
const h = client.getHierarchy()
return Reflect.get(h, p)
}
}
) as TxOperations & Client
// We need a proxy to handle all the calls to the proper client.
const clientProxy = new Proxy(
{},
{
get (target, p, receiver) {
if (p === 'getHierarchy') {
return () => hierarchyProxy
}
return Reflect.get(client, p)
}
}
) as TxOperations & Client
/** /**
* @public * @public
*/ */
export function getClient (): TxOperations & Client & OptimisticTxes { export function getClient (): TxOperations & Client {
return client return clientProxy
} }
let txQueue: Tx[] = [] let txQueue: Tx[] = []
@ -252,6 +267,7 @@ export function addRefreshListener (r: RefreshListener): void {
* @public * @public
*/ */
export async function setClient (_client: Client): Promise<void> { export async function setClient (_client: Client): Promise<void> {
pendingCreatedDocs.set({})
if (liveQuery !== undefined) { if (liveQuery !== undefined) {
await liveQuery.close() await liveQuery.close()
} }
@ -700,7 +716,7 @@ export function isSpace (space: Doc): space is Space {
} }
export function isSpaceClass (_class: Ref<Class<Doc>>): boolean { export function isSpaceClass (_class: Ref<Class<Doc>>): boolean {
return getClient().getHierarchy().isDerived(_class, core.class.Space) return client.getHierarchy().isDerived(_class, core.class.Space)
} }
export function setPresentationCookie (token: string, workspaceId: string): void { export function setPresentationCookie (token: string, workspaceId: string): void {

View File

@ -59,6 +59,9 @@
} }
function onKeydown (e: KeyboardEvent): void { function onKeydown (e: KeyboardEvent): void {
if (e.key === undefined) {
return
}
const key = e.key.toLowerCase() const key = e.key.toLowerCase()
const target = e.target as HTMLInputElement const target = e.target as HTMLInputElement
if (key !== 'backspace' && key !== 'delete') return if (key !== 'backspace' && key !== 'delete') return

View File

@ -63,7 +63,7 @@ class FocusManagerImpl implements FocusManager {
return return
} }
this.current = this.elements.findIndex((it) => it.id === idx) ?? 0 this.current = this.elements.findIndex((it) => it.id === idx) ?? 0
this.elements[Math.abs(this.current) % this.elements.length].focus() this.elements[Math.abs(this.current) % this.elements.length]?.focus()
} }
setFocusPos (order: number): void { setFocusPos (order: number): void {
@ -73,7 +73,7 @@ class FocusManagerImpl implements FocusManager {
const idx = this.elements.findIndex((it) => it.order === order) const idx = this.elements.findIndex((it) => it.order === order)
if (idx !== undefined) { if (idx !== undefined) {
this.current = idx this.current = idx
this.elements[Math.abs(this.current) % this.elements.length].focus() this.elements[Math.abs(this.current) % this.elements.length]?.focus()
} }
} }

View File

@ -149,16 +149,17 @@ export function closePopup (category?: string): void {
} else { } else {
for (let i = popups.length - 1; i >= 0; i--) { for (let i = popups.length - 1; i >= 0; i--) {
if (popups[i].type !== 'popup') continue if (popups[i].type !== 'popup') continue
if ((popups[i] as CompAndProps).options.fixed !== true) { const popi = popups[i] as CompAndProps
const isClosing = (popups[i] as CompAndProps).closing ?? false if (popi.options.fixed !== true) {
const isClosing = popi.closing ?? false
if (popups[i].type === 'popup') { if (popups[i].type === 'popup') {
;(popups[i] as CompAndProps).closing = true popi.closing = true
} }
if (!isClosing) { if (!isClosing) {
// To prevent possible recursion, we need to check if we call some code from popup close, to do close. // To prevent possible recursion, we need to check if we call some code from popup close, to do close.
;(popups[i] as CompAndProps).onClose?.(undefined) popi.onClose?.(undefined)
} }
;(popups[i] as CompAndProps).closing = false popi.closing = false
popups.splice(i, 1) popups.splice(i, 1)
break break
} }

View File

@ -214,8 +214,12 @@ export function replaceURLs (text: string): string {
* @returns {string} string with parsed URL * @returns {string} string with parsed URL
*/ */
export function parseURL (text: string): string { export function parseURL (text: string): string {
const matches = autolinker.parse(text, { urls: true }) try {
return matches.length > 0 ? matches[0].getAnchorHref() : '' const matches = autolinker.parse(text ?? '', { urls: true })
return matches.length > 0 ? matches[0].getAnchorHref() : ''
} catch (err: any) {
return ''
}
} }
/** /**

View File

@ -16,7 +16,7 @@
import contact, { Person, PersonAccount } from '@hcengineering/contact' import contact, { Person, PersonAccount } from '@hcengineering/contact'
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources' import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { Class, Doc, getCurrentAccount, Markup, Ref, Space, WithLookup } from '@hcengineering/core' import { Class, Doc, getCurrentAccount, Markup, Ref, Space, WithLookup } from '@hcengineering/core'
import { getClient, MessageViewer } from '@hcengineering/presentation' import { getClient, MessageViewer, pendingCreatedDocs } from '@hcengineering/presentation'
import { AttachmentDocList, AttachmentImageSize } from '@hcengineering/attachment-resources' import { AttachmentDocList, AttachmentImageSize } from '@hcengineering/attachment-resources'
import { getDocLinkTitle } from '@hcengineering/view-resources' import { getDocLinkTitle } from '@hcengineering/view-resources'
import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui' import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui'
@ -58,7 +58,6 @@
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
const client = getClient() const client = getClient()
const { pendingCreatedDocs } = client
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const STALE_TIMEOUT_MS = 5000 const STALE_TIMEOUT_MS = 5000
const currentAccount = getCurrentAccount() const currentAccount = getCurrentAccount()

View File

@ -82,9 +82,11 @@
included = [] included = []
} }
$: employees = Array.from( $: employees = Array.isArray(value)
(value ?? []).map((it) => $personAccountByIdStore.get(it as Ref<PersonAccount>)?.person) ? (Array.from((value ?? []).map((it) => $personAccountByIdStore.get(it as Ref<PersonAccount>)?.person)).filter(
).filter((it) => it !== undefined) as Ref<Employee>[] (it) => it !== undefined
) as Ref<Employee>[])
: []
$: docQuery = $: docQuery =
excluded.length === 0 && included.length === 0 excluded.length === 0 && included.length === 0

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Contact, Employee, Person } from '@hcengineering/contact' import contact, { Person } from '@hcengineering/contact'
import type { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core' import type { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import { ObjectCreate, getClient } from '@hcengineering/presentation' import { ObjectCreate, getClient } from '@hcengineering/presentation'
@ -43,8 +43,8 @@
export let sort: ((a: Person, b: Person) => number) | undefined = undefined export let sort: ((a: Person, b: Person) => number) | undefined = undefined
function filter (items: Ref<Person>[]): Ref<Person>[] { function filter (items: Ref<Person>[] | undefined): Ref<Person>[] {
return items.filter((it, idx, arr) => arr.indexOf(it) === idx) return (items ?? []).filter((it, idx, arr) => arr.indexOf(it) === idx)
} }
let persons: Person[] = filter(items) let persons: Person[] = filter(items)
@ -55,11 +55,10 @@
.filter((p) => p !== undefined) as Person[] .filter((p) => p !== undefined) as Person[]
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient()
async function addPerson (evt: Event): Promise<void> { async function addPerson (evt: Event): Promise<void> {
const accounts = new Set( const accounts = new Set(
client getClient()
.getModel() .getModel()
.findAllSync(contact.class.PersonAccount, {}) .findAllSync(contact.class.PersonAccount, {})
.map((p) => p.person) .map((p) => p.person)
@ -72,7 +71,7 @@
allowDeselect: false, allowDeselect: false,
selectedUsers: filter(items), selectedUsers: filter(items),
filter: (it: Doc) => { filter: (it: Doc) => {
const h = client.getHierarchy() const h = getClient().getHierarchy()
if (h.hasMixin(it, contact.mixin.Employee)) { if (h.hasMixin(it, contact.mixin.Employee)) {
const isActive = h.as(it, contact.mixin.Employee).active const isActive = h.as(it, contact.mixin.Employee).active
const isSelected = items.some((selectedItem) => selectedItem === it._id) const isSelected = items.some((selectedItem) => selectedItem === it._id)

View File

@ -46,7 +46,7 @@
if (space !== value.space) { if (space !== value.space) {
const children = await findChildren(value) const children = await findChildren(value)
for (const child of children) { for (const child of children) {
await client.updateDoc(document.class.Document, value.space, child, { await ops.updateDoc(document.class.Document, value.space, child, {
space space
}) })
} }

View File

@ -1058,7 +1058,11 @@ export async function restorePassword (token: string, password: string): Promise
} }
async function handleStatusError (message: string, err: Status): Promise<void> { async function handleStatusError (message: string, err: Status): Promise<void> {
if (err.code === platform.status.InvalidPassword || err.code === platform.status.AccountNotFound) { if (
err.code === platform.status.InvalidPassword ||
err.code === platform.status.AccountNotFound ||
err.code === platform.status.InvalidOtp
) {
// No need to send to analytics // No need to send to analytics
return return
} }

View File

@ -340,7 +340,7 @@
console.log(`Error exiting fullscreen mode: ${err.message} (${err.name})`) console.log(`Error exiting fullscreen mode: ${err.message} (${err.name})`)
$isFullScreen = false $isFullScreen = false
}) })
} else if (!document.fullscreenElement && needFullScreen) { } else if (!document.fullscreenElement && needFullScreen && roomEl != null) {
roomEl roomEl
.requestFullscreen() .requestFullscreen()
.then(() => { .then(() => {
@ -355,7 +355,7 @@
function onFullScreen (): void { function onFullScreen (): void {
const needFullScreen = !$isFullScreen const needFullScreen = !$isFullScreen
if (!document.fullscreenElement && needFullScreen) { if (!document.fullscreenElement && needFullScreen && roomEl != null) {
roomEl roomEl
.requestFullscreen() .requestFullscreen()
.then(() => { .then(() => {

View File

@ -15,7 +15,15 @@
--> -->
<script lang="ts"> <script lang="ts">
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources' import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import core, { ClassifierKind, type CollaborativeDoc, Data, Doc, Mixin, Ref } from '@hcengineering/core' import core, {
ClassifierKind,
type CollaborativeDoc,
Data,
Doc,
type MarkupBlobRef,
Mixin,
Ref
} from '@hcengineering/core'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
@ -37,7 +45,7 @@
let object: Required<Vacancy> let object: Required<Vacancy>
let rawName: string = '' let rawName: string = ''
let rawDesc: string = '' let rawDesc: string = ''
let rawFullDesc: CollaborativeDoc let rawFullDesc: MarkupBlobRef | null = null
let lastId: Ref<Vacancy> | undefined = undefined let lastId: Ref<Vacancy> | undefined = undefined
let showAllMixins = false let showAllMixins = false

View File

@ -35,10 +35,8 @@
export let groupByKey: string export let groupByKey: string
export let config: (string | BuildModelKey)[] export let config: (string | BuildModelKey)[]
const client = getClient() const assigneeAttribute = getClient().getHierarchy().getAttribute(recruit.class.Applicant, 'assignee')
const hierarchy = client.getHierarchy() const isTitleHidden = getClient().getHierarchy().getAttribute(recruit.mixin.Candidate, 'title').hidden
const assigneeAttribute = hierarchy.getAttribute(recruit.class.Applicant, 'assignee')
const isTitleHidden = client.getHierarchy().getAttribute(recruit.mixin.Candidate, 'title').hidden
$: channels = (object.$lookup?.attachedTo as WithLookup<Candidate>)?.$lookup?.channels $: channels = (object.$lookup?.attachedTo as WithLookup<Candidate>)?.$lookup?.channels
@ -70,7 +68,7 @@
<Avatar person={object.$lookup?.attachedTo} size={'medium'} name={object.$lookup?.attachedTo?.name} /> <Avatar person={object.$lookup?.attachedTo} size={'medium'} name={object.$lookup?.attachedTo?.name} />
<div class="flex-grow flex-col min-w-0 ml-2"> <div class="flex-grow flex-col min-w-0 ml-2">
<div class="fs-title over-underline lines-limit-2"> <div class="fs-title over-underline lines-limit-2">
{object.$lookup?.attachedTo ? getName(client.getHierarchy(), object.$lookup.attachedTo) : ''} {object.$lookup?.attachedTo ? getName(getClient().getHierarchy(), object.$lookup.attachedTo) : ''}
</div> </div>
{#if !isTitleHidden && enabledConfig(config, 'title')} {#if !isTitleHidden && enabledConfig(config, 'title')}
<div class="text-sm lines-limit-2">{object.$lookup?.attachedTo?.title ?? ''}</div> <div class="text-sm lines-limit-2">{object.$lookup?.attachedTo?.title ?? ''}</div>
@ -107,7 +105,7 @@
shrink={1} shrink={1}
value={object.status} value={object.status}
onChange={(status) => { onChange={(status) => {
client.update(object, { status }) getClient().update(object, { status })
}} }}
/> />
{/if} {/if}
@ -120,7 +118,7 @@
shouldRender={object.dueDate !== null && object.dueDate !== undefined} shouldRender={object.dueDate !== null && object.dueDate !== undefined}
shouldIgnoreOverdue={isDone} shouldIgnoreOverdue={isDone}
onChange={async (e) => { onChange={async (e) => {
await client.update(object, { dueDate: e }) await getClient().update(object, { dueDate: e })
}} }}
/> />
{/if} {/if}

View File

@ -122,7 +122,7 @@
} }
const handleSelect = async (event: CustomEvent): Promise<void> => { const handleSelect = async (event: CustomEvent): Promise<void> => {
selected = event.detail as AnyAttribute selected = event.detail as AnyAttribute
if (selected != null) { if (selected?._id != null) {
const exist = (await client.findOne(selected.attributeOf, { [selected.name]: { $exists: true } })) !== undefined const exist = (await client.findOne(selected.attributeOf, { [selected.name]: { $exists: true } })) !== undefined
$settingsStore = { id: selected._id, component: EditAttribute, props: { attribute: selected, exist, disabled } } $settingsStore = { id: selected._id, component: EditAttribute, props: { attribute: selected, exist, disabled } }
} }

View File

@ -55,7 +55,9 @@
specials = newSpecials specials = newSpecials
} }
$: updateSpecials(model, space) $: if (model != null) {
void updateSpecials(model, space)
}
$: visible = $: visible =
(!deselect && currentSpace !== undefined && currentSpecial !== undefined && space._id === currentSpace) || (!deselect && currentSpace !== undefined && currentSpecial !== undefined && space._id === currentSpace) ||
forciblyСollapsed forciblyСollapsed
@ -64,7 +66,7 @@
{#if specials} {#if specials}
<TreeNode <TreeNode
_id={space?._id} _id={space?._id}
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon} icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model?.icon}
iconProps={space?.icon === view.ids.IconWithEmoji iconProps={space?.icon === view.ids.IconWithEmoji
? { icon: space.color } ? { icon: space.color }
: { : {

View File

@ -108,22 +108,22 @@ export const completionConfig: Partial<CompletionOptions> = {
props: { props: {
...props, ...props,
close: () => { close: () => {
component.destroy() component?.destroy()
} }
} }
}) })
}, },
onUpdate (props: SuggestionProps) { onUpdate (props: SuggestionProps) {
component.updateProps(props) component?.updateProps(props)
}, },
onKeyDown (props: SuggestionKeyDownProps) { onKeyDown (props: SuggestionKeyDownProps) {
if (props.event.key === 'Escape') { if (props.event.key === 'Escape') {
props.event.stopPropagation() props.event.stopPropagation()
} }
return component.onKeyDown(props) return component?.onKeyDown(props)
}, },
onExit () { onExit () {
component.destroy() component?.destroy()
} }
} }
} }
@ -181,22 +181,22 @@ export function inlineCommandsConfig (
props: { props: {
...props, ...props,
close: () => { close: () => {
component.destroy() component?.destroy()
} }
} }
}) })
}, },
onUpdate (props: SuggestionProps) { onUpdate (props: SuggestionProps) {
component.updateProps(props) component?.updateProps(props)
}, },
onKeyDown (props: SuggestionKeyDownProps) { onKeyDown (props: SuggestionKeyDownProps) {
if (props.event.key === 'Escape') { if (props.event.key === 'Escape') {
props.event.stopPropagation() props.event.stopPropagation()
} }
return component.onKeyDown(props) return component?.onKeyDown(props)
}, },
onExit () { onExit () {
component.destroy() component?.destroy()
} }
} }
} }

View File

@ -55,7 +55,9 @@
specials = newSpecials specials = newSpecials
} }
$: updateSpecials(model, space) $: if (model != null) {
void updateSpecials(model, space)
}
$: visible = $: visible =
(!deselect && currentSpace !== undefined && currentSpecial !== undefined && space._id === currentSpace) || (!deselect && currentSpace !== undefined && currentSpecial !== undefined && space._id === currentSpace) ||
forciblyСollapsed forciblyСollapsed

View File

@ -30,8 +30,6 @@
let resActions = actions let resActions = actions
const client = getClient()
let loaded = false let loaded = false
const order: Record<ActionGroup, number> = { const order: Record<ActionGroup, number> = {
@ -44,7 +42,7 @@
remove: 7 remove: 7
} }
void getActions(client, object, baseMenuClass, mode).then((result) => { void getActions(getClient(), object, baseMenuClass, mode).then((result) => {
const filtered = result.filter((a) => { const filtered = result.filter((a) => {
if (excludedActions.includes(a._id)) { if (excludedActions.includes(a._id)) {
return false return false
@ -63,7 +61,7 @@
inline: a.inline, inline: a.inline,
group: a.context.group ?? 'other', group: a.context.group ?? 'other',
action: async (_: any, evt: Event) => { action: async (_: any, evt: Event) => {
if (overrides.has(a._id)) { if (overrides?.has(a._id)) {
overrides.get(a._id)?.(object, evt) overrides.get(a._id)?.(object, evt)
return return
} }

View File

@ -178,11 +178,11 @@
const keyDown = (event: KeyboardEvent, index: number) => { const keyDown = (event: KeyboardEvent, index: number) => {
if (event.key === 'ArrowDown') { if (event.key === 'ArrowDown') {
actionElements[(index + 1) % actionElements.length].focus() actionElements[(index + 1) % actionElements.length]?.focus()
} }
if (event.key === 'ArrowUp') { if (event.key === 'ArrowUp') {
actionElements[(actionElements.length + index - 1) % actionElements.length].focus() actionElements[(actionElements.length + index - 1) % actionElements.length]?.focus()
} }
if (event.key === 'ArrowLeft') { if (event.key === 'ArrowLeft') {

View File

@ -604,6 +604,7 @@ export class DocumentContentPage extends DocumentCommonPage {
await expect(this.editDocumentSpace).not.toBeVisible() await expect(this.editDocumentSpace).not.toBeVisible()
await expect(this.createNewTemplateFromSpace).not.toBeVisible() await expect(this.createNewTemplateFromSpace).not.toBeVisible()
} }
await this.page.keyboard.press('Escape')
} }
async checkSpaceFormIsCreated (spaceName: string): Promise<void> { async checkSpaceFormIsCreated (spaceName: string): Promise<void> {

View File

@ -31,6 +31,7 @@ import core, {
Data, Data,
generateId, generateId,
getWorkspaceId, getWorkspaceId,
isActiveMode,
isArchivingMode, isArchivingMode,
isMigrationMode, isMigrationMode,
isWorkspaceCreating, isWorkspaceCreating,
@ -511,7 +512,7 @@ export async function selectWorkspace (
} }
return result return result
} }
if (workspaceInfo.disabled === true && workspaceInfo.mode === 'active') { if (workspaceInfo.disabled === true && isActiveMode(workspaceInfo.mode)) {
ctx.error('workspace disabled', { workspaceUrl, email }) ctx.error('workspace disabled', { workspaceUrl, email })
throw new PlatformError( throw new PlatformError(
new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl }) new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspace: workspaceUrl })
@ -1770,14 +1771,14 @@ export async function getWorkspaceInfo (
const [ws] = await ctx.with('get-workspace', {}, async () => const [ws] = await ctx.with('get-workspace', {}, async () =>
(await db.workspace.find(query)).filter( (await db.workspace.find(query)).filter(
(it) => it.disabled !== true || account?.admin === true || it.mode !== 'active' (it) => it.disabled !== true || account?.admin === true || !isActiveMode(it.mode)
) )
) )
if (ws == null) { if (ws == null) {
ctx.error('no workspace', { workspace: workspace.name, email }) ctx.error('no workspace', { workspace: workspace.name, email })
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
} }
if (ws.mode !== 'archived' && _updateLastVisit && (isAccount(account) || email === systemAccountEmail)) { if (!isArchivingMode(ws.mode) && _updateLastVisit && (isAccount(account) || email === systemAccountEmail)) {
void ctx.with('update-last-visit', {}, () => updateLastVisit(db, ws, account as Account)) void ctx.with('update-last-visit', {}, () => updateLastVisit(db, ws, account as Account))
} }

View File

@ -204,12 +204,17 @@ test.describe('Content in the Documents tests', () => {
await expect(documentContentPage.imageInPopup()).toBeVisible() await expect(documentContentPage.imageInPopup()).toBeVisible()
await documentContentPage.fullscreenButton().click() await documentContentPage.fullscreenButton().click()
await expect(documentContentPage.fullscreenImage()).toBeVisible() await expect(documentContentPage.fullscreenImage()).toBeVisible()
await documentContentPage.page.keyboard.press('Escape') await expect(async () => {
await expect(documentContentPage.fullscreenImage()).toBeHidden() await documentContentPage.page.keyboard.press('Escape')
await expect(documentContentPage.fullscreenImage()).toBeHidden()
await expect(documentContentPage.imageInPopup()).toBeHidden()
}).toPass(retryOptions)
}) })
await test.step('User can open image original in the new tab', async () => { await test.step('User can open image original in the new tab', async () => {
const pagePromise = context.waitForEvent('page') const pagePromise = context.waitForEvent('page', {
timeout: 15000
})
await documentContentPage.clickImageOriginalButton() await documentContentPage.clickImageOriginalButton()
const newPage = await pagePromise const newPage = await pagePromise

View File

@ -29,6 +29,7 @@ test.describe('Tracker issue tests', () => {
const newIssue: NewIssue = { const newIssue: NewIssue = {
title: `Issue with all parameters and attachments-${generateId()}`, title: `Issue with all parameters and attachments-${generateId()}`,
description: 'Created issue with all parameters and attachments description', description: 'Created issue with all parameters and attachments description',
projectName: 'Default',
status: 'In Progress', status: 'In Progress',
priority: 'Urgent', priority: 'Urgent',
assignee: 'Appleseed John', assignee: 'Appleseed John',
@ -48,7 +49,8 @@ test.describe('Tracker issue tests', () => {
test('Edit an issue', async ({ page }) => { test('Edit an issue', async ({ page }) => {
const newIssue: NewIssue = { const newIssue: NewIssue = {
title: `Issue with all parameters and attachments-${generateId()}`, title: `Issue with all parameters and attachments-${generateId()}`,
description: 'Created issue with all parameters and attachments description' description: 'Created issue with all parameters and attachments description',
projectName: 'Default'
} }
const editIssue: Issue = { const editIssue: Issue = {
status: 'Done', status: 'Done',
@ -86,7 +88,8 @@ test.describe('Tracker issue tests', () => {
test.skip('Set parent issue', async ({ page }) => { test.skip('Set parent issue', async ({ page }) => {
const parentIssue: NewIssue = { const parentIssue: NewIssue = {
title: `PARENT ISSUE-${generateId(2)}`, title: `PARENT ISSUE-${generateId(2)}`,
description: 'Created issue to be parent issue' description: 'Created issue to be parent issue',
projectName: 'Default'
} }
await issuesPage.clickModelSelectorAll() await issuesPage.clickModelSelectorAll()
@ -96,7 +99,8 @@ test.describe('Tracker issue tests', () => {
const newIssue: NewIssue = { const newIssue: NewIssue = {
title: `Set parent issue during creation-${generateId(2)}`, title: `Set parent issue during creation-${generateId(2)}`,
description: 'Set parent issue during creation', description: 'Set parent issue during creation',
parentIssue: parentIssue.title parentIssue: parentIssue.title,
projectName: 'Default'
} }
await issuesPage.clickModelSelectorAll() await issuesPage.clickModelSelectorAll()
@ -116,7 +120,8 @@ test.describe('Tracker issue tests', () => {
await test.step('Set parent issue from issues page', async () => { await test.step('Set parent issue from issues page', async () => {
const newIssue: NewIssue = { const newIssue: NewIssue = {
title: `Set parent issue from issues page-${generateId(2)}`, title: `Set parent issue from issues page-${generateId(2)}`,
description: 'Set parent issue from issues page' description: 'Set parent issue from issues page',
projectName: 'Default'
} }
await issuesPage.clickModelSelectorAll() await issuesPage.clickModelSelectorAll()
await issuesPage.createNewIssue(newIssue) await issuesPage.createNewIssue(newIssue)
@ -139,7 +144,8 @@ test.describe('Tracker issue tests', () => {
await test.step('Set parent issue from issue details page', async () => { await test.step('Set parent issue from issue details page', async () => {
const newIssue: NewIssue = { const newIssue: NewIssue = {
title: `Set parent issue from issue details page-${generateId(2)}`, title: `Set parent issue from issue details page-${generateId(2)}`,
description: 'Set parent issue from issue details page' description: 'Set parent issue from issue details page',
projectName: 'Default'
} }
await issuesPage.clickModelSelectorAll() await issuesPage.clickModelSelectorAll()
await issuesPage.createNewIssue(newIssue) await issuesPage.createNewIssue(newIssue)
@ -160,7 +166,8 @@ test.describe('Tracker issue tests', () => {
const secondProjectName = 'Second Project' const secondProjectName = 'Second Project'
const moveIssue: NewIssue = { const moveIssue: NewIssue = {
title: `Issue to another project-${generateId()}`, title: `Issue to another project-${generateId()}`,
description: 'Issue to move to another project' description: 'Issue to move to another project',
projectName: 'Default'
} }
await prepareNewIssueWithOpenStep(page, moveIssue) await prepareNewIssueWithOpenStep(page, moveIssue)
await issuesDetailsPage.moreActionOnIssue('Move to project') await issuesDetailsPage.moreActionOnIssue('Move to project')
@ -181,7 +188,8 @@ test.describe('Tracker issue tests', () => {
const commentText = `Comment should be stored after reload-${generateId()}` const commentText = `Comment should be stored after reload-${generateId()}`
const commentIssue: NewIssue = { const commentIssue: NewIssue = {
title: `Issue for stored comment-${generateId()}`, title: `Issue for stored comment-${generateId()}`,
description: 'Issue for comment stored after reload the page' description: 'Issue for comment stored after reload the page',
projectName: 'Default'
} }
await prepareNewIssueWithOpenStep(page, commentIssue) await prepareNewIssueWithOpenStep(page, commentIssue)
@ -203,7 +211,8 @@ test.describe('Tracker issue tests', () => {
priority: 'Medium', priority: 'Medium',
estimation: '8', estimation: '8',
component: 'Default component', component: 'Default component',
milestone: 'Edit Milestone' milestone: 'Edit Milestone',
projectName: 'Default'
} }
await issuesPage.clickModelSelectorAll() await issuesPage.clickModelSelectorAll()
await issuesPage.buttonCreateNewIssue().click() await issuesPage.buttonCreateNewIssue().click()
@ -219,8 +228,9 @@ test.describe('Tracker issue tests', () => {
test('Delete an issue', async ({ page }) => { test('Delete an issue', async ({ page }) => {
const deleteIssue: NewIssue = { const deleteIssue: NewIssue = {
title: `Issue-to-delete-${generateId()}`, title: `Issue-to-delete-${generateId(4)}`,
description: 'Description Issue for deletion' description: 'Description Issue for deletion',
projectName: 'Default'
} }
await prepareNewIssueWithOpenStep(page, deleteIssue) await prepareNewIssueWithOpenStep(page, deleteIssue)
await issuesPage.navigateToIssues() await issuesPage.navigateToIssues()
@ -238,7 +248,8 @@ test.describe('Tracker issue tests', () => {
const additionalDescription = 'New row for the additional description' const additionalDescription = 'New row for the additional description'
const changedDescriptionIssue: NewIssue = { const changedDescriptionIssue: NewIssue = {
title: `Check the changed description activity-${generateId()}`, title: `Check the changed description activity-${generateId()}`,
description: 'Check the changed description activity description' description: 'Check the changed description activity description',
projectName: 'Default'
} }
await prepareNewIssueWithOpenStep(page, changedDescriptionIssue) await prepareNewIssueWithOpenStep(page, changedDescriptionIssue)
await issuesDetailsPage.waitDetailsOpened(changedDescriptionIssue.title) await issuesDetailsPage.waitDetailsOpened(changedDescriptionIssue.title)
@ -251,7 +262,8 @@ test.describe('Tracker issue tests', () => {
test('Add comment with image attachment', async ({ page }) => { test('Add comment with image attachment', async ({ page }) => {
const commentImageIssue: NewIssue = { const commentImageIssue: NewIssue = {
title: `Add comment with image attachment-${generateId()}`, title: `Add comment with image attachment-${generateId()}`,
description: 'Add comment with image attachment' description: 'Add comment with image attachment',
projectName: 'Default'
} }
await prepareNewIssueWithOpenStep(page, commentImageIssue) await prepareNewIssueWithOpenStep(page, commentImageIssue)
await issuesDetailsPage.waitDetailsOpened(commentImageIssue.title) await issuesDetailsPage.waitDetailsOpened(commentImageIssue.title)
@ -264,7 +276,8 @@ test.describe('Tracker issue tests', () => {
const commentPopup = `Comment for the popup-${generateId()}` const commentPopup = `Comment for the popup-${generateId()}`
const commentIssue: NewIssue = { const commentIssue: NewIssue = {
title: `Issue for add comment by popup-${generateId()}`, title: `Issue for add comment by popup-${generateId()}`,
description: 'Issue for add comment by popup' description: 'Issue for add comment by popup',
projectName: 'Default'
} }
await prepareNewIssueWithOpenStep(page, commentIssue) await prepareNewIssueWithOpenStep(page, commentIssue)
await issuesDetailsPage.waitDetailsOpened(commentIssue.title) await issuesDetailsPage.waitDetailsOpened(commentIssue.title)

View File

@ -9,7 +9,8 @@ test.describe('Tracker public link issues tests', () => {
test('Public link generate', async ({ browser }) => { test('Public link generate', async ({ browser }) => {
const publicLinkIssue: NewIssue = { const publicLinkIssue: NewIssue = {
title: `Public link generate issue-${generateId()}`, title: `Public link generate issue-${generateId()}`,
description: 'Public link generate issue' description: 'Public link generate issue',
projectName: 'Default'
} }
let link: string let link: string
@ -60,7 +61,8 @@ test.describe('Tracker public link issues tests', () => {
test('Public link Revoke', async ({ browser }) => { test('Public link Revoke', async ({ browser }) => {
const publicLinkIssue: NewIssue = { const publicLinkIssue: NewIssue = {
title: `Public link Revoke issue-${generateId()}`, title: `Public link Revoke issue-${generateId()}`,
description: 'Public link Revoke issue' description: 'Public link Revoke issue',
projectName: 'Default'
} }
const newContext = await browser.newContext({ storageState: PlatformSetting }) const newContext = await browser.newContext({ storageState: PlatformSetting })