mirror of
https://github.com/hcengineering/platform.git
synced 2025-03-19 05:08:12 +00:00
Merge remote-tracking branch 'origin/develop' into staging
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
commit
143de3c85e
@ -1139,7 +1139,8 @@ export function devTool (
|
|||||||
.option('-m, --move <move>', 'When set to true, the files will be moved, otherwise copied', 'false')
|
.option('-m, --move <move>', 'When set to true, the files will be moved, otherwise copied', 'false')
|
||||||
.option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 50mb)', '50')
|
.option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 50mb)', '50')
|
||||||
.option('-c, --concurrency <concurrency>', 'Number of files being processed concurrently', '10')
|
.option('-c, --concurrency <concurrency>', 'Number of files being processed concurrently', '10')
|
||||||
.action(async (cmd: { workspace: string, move: string, blobLimit: string, concurrency: string }) => {
|
.option('--disabled', 'Include disabled workspaces', false)
|
||||||
|
.action(async (cmd: { workspace: string, move: string, blobLimit: string, concurrency: string, disabled: boolean }) => {
|
||||||
const params = {
|
const params = {
|
||||||
blobSizeLimitMb: parseInt(cmd.blobLimit),
|
blobSizeLimitMb: parseInt(cmd.blobLimit),
|
||||||
concurrency: parseInt(cmd.concurrency),
|
concurrency: parseInt(cmd.concurrency),
|
||||||
@ -1165,6 +1166,10 @@ export function devTool (
|
|||||||
if (cmd.workspace !== '' && workspace.workspace !== cmd.workspace) {
|
if (cmd.workspace !== '' && workspace.workspace !== cmd.workspace) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (workspace.disabled === true && !cmd.disabled) {
|
||||||
|
console.log('ignore disabled workspace', workspace.workspace)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
console.log('start', workspace.workspace, index, '/', workspaces.length)
|
console.log('start', workspace.workspace, index, '/', workspaces.length)
|
||||||
await moveFiles(toolCtx, getWorkspaceId(workspace.workspace), exAdapter, params)
|
await moveFiles(toolCtx, getWorkspaceId(workspace.workspace), exAdapter, params)
|
||||||
@ -1182,7 +1187,8 @@ export function devTool (
|
|||||||
program
|
program
|
||||||
.command('sync-files')
|
.command('sync-files')
|
||||||
.option('-w, --workspace <workspace>', 'Selected workspace only', '')
|
.option('-w, --workspace <workspace>', 'Selected workspace only', '')
|
||||||
.action(async (cmd: { workspace: string }) => {
|
.option('--disabled', 'Include disabled workspaces', false)
|
||||||
|
.action(async (cmd: { workspace: string, disabled: boolean }) => {
|
||||||
const { mongodbUri } = prepareTools()
|
const { mongodbUri } = prepareTools()
|
||||||
await withDatabase(mongodbUri, async (db) => {
|
await withDatabase(mongodbUri, async (db) => {
|
||||||
await withStorage(mongodbUri, async (adapter) => {
|
await withStorage(mongodbUri, async (adapter) => {
|
||||||
@ -1196,13 +1202,22 @@ export function devTool (
|
|||||||
workspaces.sort((a, b) => b.lastVisit - a.lastVisit)
|
workspaces.sort((a, b) => b.lastVisit - a.lastVisit)
|
||||||
|
|
||||||
for (const workspace of workspaces) {
|
for (const workspace of workspaces) {
|
||||||
|
if (workspace.disabled === true && !cmd.disabled) {
|
||||||
|
console.log('ignore disabled workspace', workspace.workspace)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (cmd.workspace !== '' && workspace.workspace !== cmd.workspace) {
|
if (cmd.workspace !== '' && workspace.workspace !== cmd.workspace) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('start', workspace.workspace, index, '/', workspaces.length)
|
try {
|
||||||
await syncFiles(toolCtx, getWorkspaceId(workspace.workspace), exAdapter)
|
console.log('start', workspace.workspace, index, '/', workspaces.length)
|
||||||
console.log('done', workspace.workspace)
|
await syncFiles(toolCtx, getWorkspaceId(workspace.workspace), exAdapter)
|
||||||
|
console.log('done', workspace.workspace)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('failed to sync files', err)
|
||||||
|
}
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { type AnySvelteComponent, ButtonIcon, IconSize } from '@hcengineering/ui'
|
||||||
|
import { Asset, IntlString } from '@hcengineering/platform'
|
||||||
|
import { ComponentType } from 'svelte'
|
||||||
|
|
||||||
|
export let label: IntlString
|
||||||
|
export let icon: Asset | AnySvelteComponent | ComponentType
|
||||||
|
export let iconProps: any | undefined = undefined
|
||||||
|
export let size: IconSize = 'small'
|
||||||
|
export let action: (ev: MouseEvent) => Promise<void> | void = async () => {}
|
||||||
|
export let opened: boolean = false
|
||||||
|
export let dataId: string | undefined = undefined
|
||||||
|
|
||||||
|
function onClick (ev: MouseEvent): void {
|
||||||
|
ev.stopPropagation()
|
||||||
|
ev.preventDefault()
|
||||||
|
void action(ev)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ButtonIcon
|
||||||
|
{icon}
|
||||||
|
{iconProps}
|
||||||
|
iconSize={size}
|
||||||
|
size="small"
|
||||||
|
kind="tertiary"
|
||||||
|
pressed={opened}
|
||||||
|
{dataId}
|
||||||
|
tooltip={{ label }}
|
||||||
|
on:click={onClick}
|
||||||
|
/>
|
@ -14,29 +14,21 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Attachment } from '@hcengineering/attachment'
|
import { type Attachment } from '@hcengineering/attachment'
|
||||||
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
|
import type { WithLookup } from '@hcengineering/core'
|
||||||
import {
|
import { getResource } from '@hcengineering/platform'
|
||||||
|
import presentation, {
|
||||||
FilePreviewPopup,
|
FilePreviewPopup,
|
||||||
canPreviewFile,
|
canPreviewFile,
|
||||||
getFileUrl,
|
getFileUrl,
|
||||||
getPreviewAlignment,
|
getPreviewAlignment,
|
||||||
previewTypes
|
previewTypes
|
||||||
} from '@hcengineering/presentation'
|
} from '@hcengineering/presentation'
|
||||||
import {
|
import { IconMoreH, IconOpen, Menu, Action as UIAction, closeTooltip, showPopup, tooltip } from '@hcengineering/ui'
|
||||||
ActionIcon,
|
|
||||||
IconMoreH,
|
|
||||||
IconOpen,
|
|
||||||
Menu,
|
|
||||||
Action as UIAction,
|
|
||||||
closeTooltip,
|
|
||||||
showPopup,
|
|
||||||
tooltip
|
|
||||||
} from '@hcengineering/ui'
|
|
||||||
import view, { Action } from '@hcengineering/view'
|
import view, { Action } from '@hcengineering/view'
|
||||||
|
|
||||||
import type { WithLookup } from '@hcengineering/core'
|
import AttachmentAction from './AttachmentAction.svelte'
|
||||||
import attachmentPlugin from '../plugin'
|
|
||||||
import FileDownload from './icons/FileDownload.svelte'
|
import FileDownload from './icons/FileDownload.svelte'
|
||||||
|
import attachmentPlugin from '../plugin'
|
||||||
|
|
||||||
export let attachment: WithLookup<Attachment>
|
export let attachment: WithLookup<Attachment>
|
||||||
export let isSaved = false
|
export let isSaved = false
|
||||||
@ -131,30 +123,37 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
{#if canPreview}
|
||||||
|
<AttachmentAction
|
||||||
|
label={view.string.Open}
|
||||||
|
icon={IconOpen}
|
||||||
|
size="small"
|
||||||
|
dataId="open-in-sidebar"
|
||||||
|
action={showPreview}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<a
|
<a
|
||||||
class="mr-1 flex-row-center gap-2 p-1"
|
|
||||||
href={getFileUrl(attachment.file, attachment.name)}
|
href={getFileUrl(attachment.file, attachment.name)}
|
||||||
download={attachment.name}
|
download={attachment.name}
|
||||||
bind:this={download}
|
bind:this={download}
|
||||||
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
|
use:tooltip={{ label: presentation.string.Download }}
|
||||||
on:click|stopPropagation
|
on:click|stopPropagation
|
||||||
>
|
>
|
||||||
{#if canPreview}
|
<AttachmentAction
|
||||||
<ActionIcon
|
label={presentation.string.Download}
|
||||||
icon={IconOpen}
|
|
||||||
size={'medium'}
|
|
||||||
action={(evt) => {
|
|
||||||
showPreview(evt)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<ActionIcon
|
|
||||||
icon={FileDownload}
|
icon={FileDownload}
|
||||||
size={'medium'}
|
size="small"
|
||||||
|
dataId="open-in-sidebar"
|
||||||
action={() => {
|
action={() => {
|
||||||
download.click()
|
download.click()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} />
|
<AttachmentAction
|
||||||
|
label={view.string.MoreActions}
|
||||||
|
icon={IconMoreH}
|
||||||
|
size="small"
|
||||||
|
dataId="open-in-sidebar"
|
||||||
|
action={showMenu}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
interface Dimensions {
|
interface Dimensions {
|
||||||
width: 'auto' | number
|
width: 'auto' | number
|
||||||
height: 'auto' | number
|
height: 'auto' | number
|
||||||
|
fit: 'cover' | 'contain'
|
||||||
}
|
}
|
||||||
|
|
||||||
const minSizeRem = 4
|
const minSizeRem = 4
|
||||||
@ -45,7 +46,8 @@
|
|||||||
if (size === 'auto') {
|
if (size === 'auto') {
|
||||||
return {
|
return {
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: 'auto'
|
height: 'auto',
|
||||||
|
fit: 'contain'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,42 +57,41 @@
|
|||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
return {
|
return {
|
||||||
width: preferredWidth,
|
width: preferredWidth,
|
||||||
height: preferredWidth
|
height: preferredWidth,
|
||||||
|
fit: 'contain'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { originalWidth, originalHeight } = metadata
|
const { originalWidth, originalHeight } = metadata
|
||||||
const maxSize = maxSizeRem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||||
|
const maxSize = maxSizeRem * fontSize
|
||||||
|
const minSize = minSizeRem * fontSize
|
||||||
|
|
||||||
const width = Math.min(originalWidth, preferredWidth)
|
const width = Math.min(originalWidth, preferredWidth)
|
||||||
const ratio = originalHeight / originalWidth
|
const ratio = originalHeight / originalWidth
|
||||||
const height = width * ratio
|
const height = width * ratio
|
||||||
|
|
||||||
|
const fit = width < minSize || height < minSize ? 'cover' : 'contain'
|
||||||
|
|
||||||
if (height > maxSize) {
|
if (height > maxSize) {
|
||||||
return {
|
return {
|
||||||
width: maxSize / ratio,
|
width: maxSize / ratio,
|
||||||
height: maxSize
|
height: maxSize,
|
||||||
|
fit
|
||||||
|
}
|
||||||
|
} else if (height < minSize) {
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height: minSize,
|
||||||
|
fit
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getObjectFit (size: Dimensions): 'contain' | 'cover' {
|
|
||||||
if (size.width === 'auto' || size.height === 'auto') {
|
|
||||||
return 'contain'
|
|
||||||
}
|
|
||||||
|
|
||||||
const minSize = minSizeRem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
|
||||||
|
|
||||||
if (size.width < minSize || size.height < minSize) {
|
|
||||||
return 'cover'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'contain'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUrlSize (size: AttachmentImageSize): IconSize {
|
function getUrlSize (size: AttachmentImageSize): IconSize {
|
||||||
@ -110,9 +111,9 @@
|
|||||||
{#await getBlobRef(value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
|
{#await getBlobRef(value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
|
||||||
<img
|
<img
|
||||||
src={blobSrc.src}
|
src={blobSrc.src}
|
||||||
style:object-fit={getObjectFit(dimensions)}
|
style:object-fit={dimensions.fit}
|
||||||
width={dimensions.width}
|
width="100%"
|
||||||
height={dimensions.height}
|
height="100%"
|
||||||
srcset={blobSrc.srcset}
|
srcset={blobSrc.srcset}
|
||||||
alt={value.name}
|
alt={value.name}
|
||||||
/>
|
/>
|
||||||
@ -124,7 +125,6 @@
|
|||||||
max-width: 20rem;
|
max-width: 20rem;
|
||||||
max-height: 20rem;
|
max-height: 20rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
object-fit: contain;
|
|
||||||
min-height: 4rem;
|
min-height: 4rem;
|
||||||
min-width: 4rem;
|
min-width: 4rem;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||||
// Copyright © 2021 Hardcore Engineering Inc.
|
// Copyright © 2021, 2024 Hardcore Engineering Inc.
|
||||||
//
|
//
|
||||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License. You may
|
// you may not use this file except in compliance with the License. You may
|
||||||
@ -58,28 +58,28 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AttachmentImagePreview {value} size={imageSize} />
|
<AttachmentImagePreview {value} size={imageSize} />
|
||||||
<div class="actions conner">
|
<div class="actions">
|
||||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if type === 'audio'}
|
{:else if type === 'audio'}
|
||||||
<div class="buttonContainer">
|
<div class="buttonContainer">
|
||||||
<AudioPlayer {value} />
|
<AudioPlayer {value} />
|
||||||
<div class="actions conner" style:padding={'0.125rem 0.25rem'}>
|
<div class="actions" style:padding={'0.125rem 0.25rem'}>
|
||||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if type === 'video'}
|
{:else if type === 'video'}
|
||||||
<div class="content buttonContainer flex-center">
|
<div class="content buttonContainer flex-center">
|
||||||
<AttachmentVideoPreview {value} preload={videoPreload} />
|
<AttachmentVideoPreview {value} preload={videoPreload} />
|
||||||
<div class="actions conner">
|
<div class="actions">
|
||||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex buttonContainer extraWidth">
|
<div class="flex buttonContainer extraWidth">
|
||||||
<AttachmentPresenter {value} />
|
<AttachmentPresenter {value} />
|
||||||
<div class="actions conner">
|
<div class="actions">
|
||||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -100,17 +100,10 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.25rem;
|
top: 0.25rem;
|
||||||
right: 0.25rem;
|
right: 0.25rem;
|
||||||
padding: 0.25rem;
|
padding: 0.125rem;
|
||||||
background-color: var(--theme-comp-header-color);
|
background-color: var(--theme-comp-header-color);
|
||||||
border: 1px solid var(--theme-divider-color);
|
border: 1px solid var(--theme-divider-color);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.375rem;
|
||||||
|
|
||||||
&.conner {
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 0.25rem;
|
|
||||||
border-radius: 0 0.4rem 0 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,9 +500,9 @@
|
|||||||
updateSelectedDate()
|
updateSelectedDate()
|
||||||
|
|
||||||
if (selectedMessageId !== undefined && messages.some(({ _id }) => _id === selectedMessageId)) {
|
if (selectedMessageId !== undefined && messages.some(({ _id }) => _id === selectedMessageId)) {
|
||||||
isScrollInitialized = true
|
|
||||||
await wait()
|
await wait()
|
||||||
scrollToMessage()
|
scrollToMessage()
|
||||||
|
isScrollInitialized = true
|
||||||
isInitialScrolling = false
|
isInitialScrolling = false
|
||||||
} else if (separatorIndex === -1) {
|
} else if (separatorIndex === -1) {
|
||||||
await wait()
|
await wait()
|
||||||
@ -800,6 +800,7 @@
|
|||||||
bind:divBox={scrollContentBox}
|
bind:divBox={scrollContentBox}
|
||||||
noStretch={false}
|
noStretch={false}
|
||||||
disableOverscroll
|
disableOverscroll
|
||||||
|
horizontal={false}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
onResize={handleResize}
|
onResize={handleResize}
|
||||||
>
|
>
|
||||||
|
@ -143,6 +143,8 @@
|
|||||||
|
|
||||||
dispatch('channel')
|
dispatch('channel')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: messagesStore = dataProvider?.messagesStore
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showHeader}
|
{#if showHeader}
|
||||||
@ -167,10 +169,13 @@
|
|||||||
<ThreadParentMessage {message} />
|
<ThreadParentMessage {message} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if message.replies && message.replies > 0}
|
{#if (message.replies ?? $messagesStore?.length ?? 0) > 0}
|
||||||
<div class="separator">
|
<div class="separator">
|
||||||
<div class="label lower">
|
<div class="label lower">
|
||||||
<Label label={activity.string.RepliesCount} params={{ replies: message.replies }} />
|
<Label
|
||||||
|
label={activity.string.RepliesCount}
|
||||||
|
params={{ replies: message.replies ?? $messagesStore?.length ?? 1 }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="line" />
|
<div class="line" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -150,6 +150,7 @@ export async function buildThreadLink (
|
|||||||
loc.path[2] = chunterId
|
loc.path[2] = chunterId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loc.query = { message: '' }
|
||||||
loc.path[3] = objectURI
|
loc.path[3] = objectURI
|
||||||
loc.path[4] = threadParent
|
loc.path[4] = threadParent
|
||||||
loc.fragment = undefined
|
loc.fragment = undefined
|
||||||
@ -177,7 +178,7 @@ export async function replyToThread (message: ActivityMessage, e: Event): Promis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void openThreadInSidebar(message._id, message)
|
await openThreadInSidebar(message._id, message)
|
||||||
if (loc.path[2] !== chunterId && loc.path[2] !== notificationId) {
|
if (loc.path[2] !== chunterId && loc.path[2] !== notificationId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -326,10 +327,14 @@ export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: Acti
|
|||||||
const name = (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {}))
|
const name = (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {}))
|
||||||
const tabName = await translate(chunter.string.ThreadIn, { name })
|
const tabName = await translate(chunter.string.ThreadIn, { name })
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
const allowedPath = loc.path.join('/')
|
|
||||||
|
|
||||||
const currentTAbs = get(sidebarStore).widgetsState.get(widget._id)?.tabs ?? []
|
if (loc.path[2] === chunterId || loc.path[2] === notificationId) {
|
||||||
const tabsToClose = currentTAbs.filter((t) => t.isPinned !== true && t.allowedPath === allowedPath).map((t) => t.id)
|
loc.path[4] = message._id
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedPath = loc.path.join('/')
|
||||||
|
const currentTabs = get(sidebarStore).widgetsState.get(widget._id)?.tabs ?? []
|
||||||
|
const tabsToClose = currentTabs.filter((t) => t.isPinned !== true && t.allowedPath === allowedPath).map((t) => t.id)
|
||||||
|
|
||||||
if (tabsToClose.length > 0) {
|
if (tabsToClose.length > 0) {
|
||||||
sidebarStore.update((s) => {
|
sidebarStore.update((s) => {
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"Remove": "Удалить нотификацию",
|
"Remove": "Удалить нотификацию",
|
||||||
"RemoveAll": "Удалить все нотификации",
|
"RemoveAll": "Удалить все нотификации",
|
||||||
"MarkAllAsRead": "Отметить все нотификации как прочитанные",
|
"MarkAllAsRead": "Отметить все нотификации как прочитанные",
|
||||||
"MarkAsRead": "Отметить нотификация прочитанной",
|
"MarkAsRead": "Отметить прочитанным",
|
||||||
"MarkAsUnread": "Отметить непрочитанным",
|
"MarkAsUnread": "Отметить непрочитанным",
|
||||||
"Archive": "Архивировать",
|
"Archive": "Архивировать",
|
||||||
"Inbox": "Входящие",
|
"Inbox": "Входящие",
|
||||||
|
@ -198,7 +198,7 @@
|
|||||||
<div
|
<div
|
||||||
class="card"
|
class="card"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dispatch('click', { context: value })
|
dispatch('click', { context: value, object })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
@ -266,7 +266,7 @@
|
|||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
dispatch('click', { context: value, notification: group[0] })
|
dispatch('click', { context: value, notification: group[0], object })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -256,7 +256,7 @@
|
|||||||
|
|
||||||
const selectedNotification: InboxNotification | undefined = event?.detail?.notification
|
const selectedNotification: InboxNotification | undefined = event?.detail?.notification
|
||||||
|
|
||||||
void selectInboxContext(linkProviders, selectedContext, selectedNotification)
|
void selectInboxContext(linkProviders, selectedContext, selectedNotification, event?.detail.object)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSelectedPanel (selectedContext?: DocNotifyContext): Promise<void> {
|
async function updateSelectedPanel (selectedContext?: DocNotifyContext): Promise<void> {
|
||||||
@ -373,6 +373,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
$: $deviceInfo.replacedPanel = replacedPanel
|
$: $deviceInfo.replacedPanel = replacedPanel
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
$deviceInfo.replacedPanel = undefined
|
$deviceInfo.replacedPanel = undefined
|
||||||
unsubscribeLoc()
|
unsubscribeLoc()
|
||||||
|
@ -583,7 +583,8 @@ export function resetInboxContext (): void {
|
|||||||
export async function selectInboxContext (
|
export async function selectInboxContext (
|
||||||
linkProviders: LinkIdProvider[],
|
linkProviders: LinkIdProvider[],
|
||||||
context: DocNotifyContext,
|
context: DocNotifyContext,
|
||||||
notification?: WithLookup<InboxNotification>
|
notification?: WithLookup<InboxNotification>,
|
||||||
|
object?: Doc
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -607,24 +608,42 @@ export async function selectInboxContext (
|
|||||||
const message = (notification as WithLookup<ActivityInboxNotification>)?.$lookup?.attachedTo
|
const message = (notification as WithLookup<ActivityInboxNotification>)?.$lookup?.attachedTo
|
||||||
|
|
||||||
if (objectClass === chunter.class.ThreadMessage) {
|
if (objectClass === chunter.class.ThreadMessage) {
|
||||||
const thread = await client.findOne(
|
const thread =
|
||||||
chunter.class.ThreadMessage,
|
object?._id === objectId
|
||||||
{
|
? (object as ThreadMessage)
|
||||||
_id: objectId as Ref<ThreadMessage>
|
: await client.findOne(
|
||||||
},
|
chunter.class.ThreadMessage,
|
||||||
{ projection: { _id: 1, attachedTo: 1 } }
|
{
|
||||||
)
|
_id: objectId as Ref<ThreadMessage>
|
||||||
|
},
|
||||||
|
{ projection: { _id: 1, attachedTo: 1 } }
|
||||||
|
)
|
||||||
|
|
||||||
void navigateToInboxDoc(linkProviders, objectId, objectClass, thread?.attachedTo, thread?._id)
|
void navigateToInboxDoc(
|
||||||
|
linkProviders,
|
||||||
|
thread?.objectId ?? objectId,
|
||||||
|
thread?.objectClass ?? objectClass,
|
||||||
|
thread?.attachedTo,
|
||||||
|
thread?._id
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReactionMessage(message)) {
|
if (isReactionMessage(message)) {
|
||||||
const thread = loc.path[4] === objectId ? objectId : undefined
|
const thread = loc.path[4] === objectId ? objectId : undefined
|
||||||
|
const reactedTo =
|
||||||
|
(object as ActivityMessage) ??
|
||||||
|
(await client.findOne(activity.class.ActivityMessage, { _id: message.attachedTo as Ref<ActivityMessage> }))
|
||||||
|
const isThread = hierarchy.isDerived(reactedTo._class, chunter.class.ThreadMessage)
|
||||||
|
const channelId = isThread ? (reactedTo as ThreadMessage)?.objectId : reactedTo?.attachedTo ?? objectId
|
||||||
|
const channelClass = isThread
|
||||||
|
? (reactedTo as ThreadMessage)?.objectClass
|
||||||
|
: reactedTo?.attachedToClass ?? objectClass
|
||||||
|
|
||||||
void navigateToInboxDoc(
|
void navigateToInboxDoc(
|
||||||
linkProviders,
|
linkProviders,
|
||||||
objectId,
|
channelId,
|
||||||
objectClass,
|
channelClass,
|
||||||
thread as Ref<ActivityMessage>,
|
thread as Ref<ActivityMessage>,
|
||||||
objectId as Ref<ActivityMessage>
|
objectId as Ref<ActivityMessage>
|
||||||
)
|
)
|
||||||
@ -633,11 +652,13 @@ export async function selectInboxContext (
|
|||||||
|
|
||||||
const selectedMsg = (notification as ActivityInboxNotification)?.attachedTo
|
const selectedMsg = (notification as ActivityInboxNotification)?.attachedTo
|
||||||
const thread = selectedMsg !== objectId ? objectId : loc.path[4] === objectId ? objectId : undefined
|
const thread = selectedMsg !== objectId ? objectId : loc.path[4] === objectId ? objectId : undefined
|
||||||
|
const channelId = (object as ActivityMessage)?.attachedTo ?? message?.attachedTo ?? objectId
|
||||||
|
const channelClass = (object as ActivityMessage)?.attachedToClass ?? message?.attachedToClass ?? objectClass
|
||||||
|
|
||||||
void navigateToInboxDoc(
|
void navigateToInboxDoc(
|
||||||
linkProviders,
|
linkProviders,
|
||||||
objectId,
|
channelId,
|
||||||
objectClass,
|
channelClass,
|
||||||
thread as Ref<ActivityMessage>,
|
thread as Ref<ActivityMessage>,
|
||||||
selectedMsg ?? (objectId as Ref<ActivityMessage>)
|
selectedMsg ?? (objectId as Ref<ActivityMessage>)
|
||||||
)
|
)
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
"@hcengineering/platform": "^0.6.11",
|
"@hcengineering/platform": "^0.6.11",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
"@hcengineering/ui": "^0.6.15",
|
"@hcengineering/ui": "^0.6.15",
|
||||||
|
"@hcengineering/analytics": "^0.6.0",
|
||||||
"@hcengineering/presentation": "^0.6.3",
|
"@hcengineering/presentation": "^0.6.3",
|
||||||
"@hcengineering/core": "^0.6.32",
|
"@hcengineering/core": "^0.6.32",
|
||||||
"@hcengineering/view": "^0.6.13",
|
"@hcengineering/view": "^0.6.13",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
//
|
//
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import { type Space, type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
|
import { type Space, type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
|
||||||
import { IntlString, translate } from '@hcengineering/platform'
|
import { IntlString, translate } from '@hcengineering/platform'
|
||||||
import { getFileUrl, getImageSize, imageSizeToRatio } from '@hcengineering/presentation'
|
import { getFileUrl, getImageSize, imageSizeToRatio } from '@hcengineering/presentation'
|
||||||
@ -116,11 +117,12 @@
|
|||||||
objectAttr
|
objectAttr
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let contentError = false
|
||||||
let localSynced = false
|
let localSynced = false
|
||||||
let remoteSynced = false
|
let remoteSynced = false
|
||||||
|
|
||||||
$: loading = !localSynced && !remoteSynced
|
$: loading = !localSynced && !remoteSynced
|
||||||
$: editable = !readonly && remoteSynced
|
$: editable = !readonly && !contentError && remoteSynced
|
||||||
|
|
||||||
void localProvider.loaded.then(() => (localSynced = true))
|
void localProvider.loaded.then(() => (localSynced = true))
|
||||||
void remoteProvider.loaded.then(() => (remoteSynced = true))
|
void remoteProvider.loaded.then(() => (remoteSynced = true))
|
||||||
@ -218,7 +220,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: if (editor !== undefined) {
|
$: if (editor !== undefined) {
|
||||||
editor.setEditable(editable, true)
|
// When the content is invalid, we don't want to emit an update
|
||||||
|
// Preventing synchronization of the invalid content
|
||||||
|
const emitUpdate = !contentError
|
||||||
|
editor.setEditable(editable, emitUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: should be inside the editor
|
// TODO: should be inside the editor
|
||||||
@ -364,6 +369,7 @@
|
|||||||
await ph
|
await ph
|
||||||
|
|
||||||
editor = new Editor({
|
editor = new Editor({
|
||||||
|
enableContentCheck: true,
|
||||||
element,
|
element,
|
||||||
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) },
|
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) },
|
||||||
extensions: [
|
extensions: [
|
||||||
@ -432,6 +438,11 @@
|
|||||||
|
|
||||||
throttle.call(updateLastUpdateTime)
|
throttle.call(updateLastUpdateTime)
|
||||||
dispatch('update')
|
dispatch('update')
|
||||||
|
},
|
||||||
|
onContentError: ({ error, disableCollaboration }) => {
|
||||||
|
disableCollaboration()
|
||||||
|
contentError = true
|
||||||
|
Analytics.handleError(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import { Markup } from '@hcengineering/core'
|
import { Markup } from '@hcengineering/core'
|
||||||
import { IntlString, translate } from '@hcengineering/platform'
|
import { IntlString, translate } from '@hcengineering/platform'
|
||||||
import { EmptyMarkup, getMarkup, markupToJSON } from '@hcengineering/text'
|
import { EmptyMarkup, getMarkup, markupToJSON } from '@hcengineering/text'
|
||||||
@ -150,6 +151,7 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
void ph.then(async () => {
|
void ph.then(async () => {
|
||||||
editor = new Editor({
|
editor = new Editor({
|
||||||
|
enableContentCheck: true,
|
||||||
element,
|
element,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: mergeAttributes(defaultEditorAttributes, editorAttributes),
|
attributes: mergeAttributes(defaultEditorAttributes, editorAttributes),
|
||||||
@ -197,6 +199,9 @@
|
|||||||
content = getContent()
|
content = getContent()
|
||||||
dispatch('value', content)
|
dispatch('value', content)
|
||||||
dispatch('update', content)
|
dispatch('update', content)
|
||||||
|
},
|
||||||
|
onContentError: ({ error }) => {
|
||||||
|
Analytics.handleError(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
} from '@hcengineering/text-editor'
|
} from '@hcengineering/text-editor'
|
||||||
import { createQuery } from '@hcengineering/presentation'
|
import { createQuery } from '@hcengineering/presentation'
|
||||||
import { getResource } from '@hcengineering/platform'
|
import { getResource } from '@hcengineering/platform'
|
||||||
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
import { inlineToolbarKey } from './extension/inlineToolbar'
|
import { inlineToolbarKey } from './extension/inlineToolbar'
|
||||||
import TextActionButton from './TextActionButton.svelte'
|
import TextActionButton from './TextActionButton.svelte'
|
||||||
@ -93,11 +94,40 @@
|
|||||||
$: categories.forEach((category) => {
|
$: categories.forEach((category) => {
|
||||||
category.sort((a, b) => a[0] - b[0])
|
category.sort((a, b) => a[0] - b[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let selecting = false
|
||||||
|
|
||||||
|
function handleMouseDown (): void {
|
||||||
|
function handleMouseMove (): void {
|
||||||
|
if (!editor.state.selection.empty) {
|
||||||
|
selecting = true
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp (): void {
|
||||||
|
selecting = false
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener('mousedown', handleMouseDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
document.removeEventListener('mousedown', handleMouseDown)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={toolbar} style="visibility: hidden;">
|
<div bind:this={toolbar} class="p-2" style="visibility: hidden;">
|
||||||
{#if editor && visible && visibleActions.length > 0}
|
{#if editor && visible && !selecting && visibleActions.length > 0}
|
||||||
<div class="text-editor-toolbar buttons-group xsmall-gap mb-4">
|
<div class="text-editor-toolbar buttons-group xsmall-gap">
|
||||||
{#each Object.values(categories) as category, index}
|
{#each Object.values(categories) as category, index}
|
||||||
{#if index > 0}
|
{#if index > 0}
|
||||||
<div class="buttons-divider" />
|
<div class="buttons-divider" />
|
||||||
@ -113,8 +143,7 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.text-editor-toolbar {
|
.text-editor-toolbar {
|
||||||
margin: -0.5rem -0.25rem 0.5rem;
|
padding: 0.25rem;
|
||||||
padding: 0.375rem;
|
|
||||||
background-color: var(--theme-comp-header-color);
|
background-color: var(--theme-comp-header-color);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
box-shadow: var(--button-shadow);
|
box-shadow: var(--button-shadow);
|
||||||
|
@ -21,6 +21,7 @@ import { type Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|||||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
|
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
|
||||||
import { common, createLowlight } from 'lowlight'
|
import { common, createLowlight } from 'lowlight'
|
||||||
|
import { isChangeEditable } from './editable'
|
||||||
|
|
||||||
type Lowlight = ReturnType<typeof createLowlight>
|
type Lowlight = ReturnType<typeof createLowlight>
|
||||||
|
|
||||||
@ -117,7 +118,7 @@ export function LanguageSelector (options: CodeBlockLowlightOptions): Plugin {
|
|||||||
return createDecorations(state.doc, options)
|
return createDecorations(state.doc, options)
|
||||||
},
|
},
|
||||||
apply (tr, prev) {
|
apply (tr, prev) {
|
||||||
if (tr.docChanged) {
|
if (tr.docChanged || isChangeEditable(tr)) {
|
||||||
return createDecorations(tr.doc, options)
|
return createDecorations(tr.doc, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ function createDecorations (doc: ProseMirrorNode, options: CodeBlockLowlightOpti
|
|||||||
doc.descendants((node, pos) => {
|
doc.descendants((node, pos) => {
|
||||||
if (node.type.name === CodeBlockLowlight.name) {
|
if (node.type.name === CodeBlockLowlight.name) {
|
||||||
decorations.push(
|
decorations.push(
|
||||||
Decoration.widget(pos + node.nodeSize - 1, (view) => {
|
Decoration.widget(pos + 1, (view) => {
|
||||||
const button = createLangButton(node.attrs.language)
|
const button = createLangButton(node.attrs.language)
|
||||||
|
|
||||||
if (view.editable) {
|
if (view.editable) {
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Extension } from '@tiptap/core'
|
||||||
|
import { type Transaction } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
const metaKey = '$editable'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
export interface EditableOptions {}
|
||||||
|
|
||||||
|
export interface EditableStorage {
|
||||||
|
isEditable: boolean | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChangeEditable (tr: Transaction): boolean {
|
||||||
|
return tr.getMeta(metaKey) !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditableExtension = Extension.create<EditableOptions, EditableStorage>({
|
||||||
|
name: 'editable',
|
||||||
|
|
||||||
|
addStorage () {
|
||||||
|
return { isEditable: undefined }
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdate () {
|
||||||
|
if (this.editor.isEditable !== this.storage.isEditable) {
|
||||||
|
const { state, view } = this.editor
|
||||||
|
|
||||||
|
this.storage.isEditable = this.editor.isEditable
|
||||||
|
const tr = state.tr.setMeta(metaKey, this.storage.isEditable)
|
||||||
|
view.dispatch(tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -22,6 +22,7 @@ export interface FocusStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FocusExtension = Extension.create<FocusOptions, FocusStorage>({
|
export const FocusExtension = Extension.create<FocusOptions, FocusStorage>({
|
||||||
|
name: 'focus',
|
||||||
addStorage () {
|
addStorage () {
|
||||||
return { canBlur: true }
|
return { canBlur: true }
|
||||||
},
|
},
|
||||||
|
@ -6,6 +6,7 @@ export interface SubmitOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SubmitExtension = Extension.create<SubmitOptions>({
|
export const SubmitExtension = Extension.create<SubmitOptions>({
|
||||||
|
name: 'submit',
|
||||||
addKeyboardShortcuts () {
|
addKeyboardShortcuts () {
|
||||||
const shortcuts: Record<string, KeyboardShortcutCommand> = {
|
const shortcuts: Record<string, KeyboardShortcutCommand> = {
|
||||||
Space: () => {
|
Space: () => {
|
||||||
|
@ -23,6 +23,7 @@ import ListKeymap from '@tiptap/extension-list-keymap'
|
|||||||
import TableHeader from '@tiptap/extension-table-header'
|
import TableHeader from '@tiptap/extension-table-header'
|
||||||
import 'prosemirror-codemark/dist/codemark.css'
|
import 'prosemirror-codemark/dist/codemark.css'
|
||||||
|
|
||||||
|
import { EditableExtension } from '../components/extension/editable'
|
||||||
import { CodeBlockHighlighExtension, codeBlockHighlightOptions } from '../components/extension/codeblock'
|
import { CodeBlockHighlighExtension, codeBlockHighlightOptions } from '../components/extension/codeblock'
|
||||||
import { NoteExtension, type NoteOptions } from '../components/extension/note'
|
import { NoteExtension, type NoteOptions } from '../components/extension/note'
|
||||||
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
|
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
|
||||||
@ -172,6 +173,7 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
[110, EditableExtension],
|
||||||
[200, CodeBlockHighlighExtension.configure(codeBlockHighlightOptions)],
|
[200, CodeBlockHighlighExtension.configure(codeBlockHighlightOptions)],
|
||||||
[210, CodeExtension.configure(codeOptions)],
|
[210, CodeExtension.configure(codeOptions)],
|
||||||
[220, HardBreakExtension.configure({ shortcuts: mode })]
|
[220, HardBreakExtension.configure({ shortcuts: mode })]
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
|
||||||
import presentation from '@hcengineering/presentation'
|
|
||||||
|
|
||||||
import { type Doc as YDoc } from 'yjs'
|
import { type Doc as YDoc } from 'yjs'
|
||||||
import { IndexeddbPersistence } from 'y-indexeddb'
|
import { IndexeddbPersistence } from 'y-indexeddb'
|
||||||
@ -26,11 +24,7 @@ export class IndexeddbProvider extends IndexeddbPersistence implements Provider
|
|||||||
readonly awareness: Awareness | null = null
|
readonly awareness: Awareness | null = null
|
||||||
|
|
||||||
constructor (documentId: string, doc: YDoc) {
|
constructor (documentId: string, doc: YDoc) {
|
||||||
const workspaceId: string = getMetadata(presentation.metadata.WorkspaceId) ?? ''
|
super(documentId, doc)
|
||||||
|
|
||||||
const name = `${workspaceId}/${documentId}`
|
|
||||||
|
|
||||||
super(name, doc)
|
|
||||||
|
|
||||||
this.loaded = new Promise((resolve) => {
|
this.loaded = new Promise((resolve) => {
|
||||||
this.on('synced', resolve)
|
this.on('synced', resolve)
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
$: if (query) updateSearchQuery(search)
|
$: if (query) updateSearchQuery(search)
|
||||||
let resultQuery: DocumentQuery<Issue> = { ...searchQuery }
|
let resultQuery: DocumentQuery<Issue> = { ...searchQuery }
|
||||||
|
|
||||||
$: if (!label && title) {
|
$: if (title) {
|
||||||
void translate(title, {}, $themeStore.language).then((res) => {
|
void translate(title, {}, $themeStore.language).then((res) => {
|
||||||
label = res
|
label = res
|
||||||
})
|
})
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
getCurrentLocation
|
getCurrentLocation
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { onDestroy } from 'svelte'
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
import { closeWidgetTab, sidebarStore, SidebarVariant, WidgetState, openWidgetTab, closeWidget } from '../../sidebar'
|
import { closeWidgetTab, sidebarStore, SidebarVariant, WidgetState, openWidgetTab, closeWidget } from '../../sidebar'
|
||||||
import WidgetsBar from './widgets/WidgetsBar.svelte'
|
import WidgetsBar from './widgets/WidgetsBar.svelte'
|
||||||
@ -46,36 +46,15 @@
|
|||||||
$: widgetState = widget !== undefined ? $sidebarStore.widgetsState.get(widget._id) : undefined
|
$: widgetState = widget !== undefined ? $sidebarStore.widgetsState.get(widget._id) : undefined
|
||||||
|
|
||||||
$: tabId = widgetState?.tab
|
$: tabId = widgetState?.tab
|
||||||
$: tabs = getTabs(widget, widgetState)
|
$: tabs = widgetState?.tabs ?? []
|
||||||
$: tab = tabId !== undefined ? tabs.find((it) => it.id === tabId) ?? tabs[0] : tabs[0]
|
$: tab = tabId !== undefined ? tabs.find((it) => it.id === tabId) ?? tabs[0] : tabs[0]
|
||||||
|
|
||||||
$: if ($sidebarStore.widget === undefined) {
|
$: if ($sidebarStore.widget === undefined) {
|
||||||
sidebarStore.update((s) => ({ ...s, variant: SidebarVariant.MINI }))
|
sidebarStore.update((s) => ({ ...s, variant: SidebarVariant.MINI }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabs (widget?: Widget, state?: WidgetState): WidgetTab[] {
|
function closeWrongTabs (loc: Location): void {
|
||||||
if (widget === undefined || !state?.tabs) return []
|
|
||||||
const loc = getCurrentLocation()
|
|
||||||
|
|
||||||
const result: WidgetTab[] = []
|
|
||||||
for (const tab of state.tabs) {
|
|
||||||
if (tab.allowedPath !== undefined && !tab.isPinned) {
|
|
||||||
const path = loc.path.join('/')
|
|
||||||
if (!path.startsWith(tab.allowedPath)) {
|
|
||||||
void handleTabClose(tab.id, widget)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribe = locationStore.subscribe((loc: Location) => {
|
|
||||||
if (widget === undefined) return
|
if (widget === undefined) return
|
||||||
|
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
if (tab.allowedPath !== undefined && !tab.isPinned) {
|
if (tab.allowedPath !== undefined && !tab.isPinned) {
|
||||||
const path = loc.path.join('/')
|
const path = loc.path.join('/')
|
||||||
@ -84,6 +63,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribe = locationStore.subscribe((loc: Location) => {
|
||||||
|
closeWrongTabs(loc)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
closeWrongTabs(getCurrentLocation())
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
@ -203,7 +203,6 @@ export function openWidgetTab (widget: Ref<Widget>, tab: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false): void {
|
export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false): void {
|
||||||
openWidget(widget)
|
|
||||||
const state = get(sidebarStore)
|
const state = get(sidebarStore)
|
||||||
const { widgetsState } = state
|
const { widgetsState } = state
|
||||||
const widgetState = widgetsState.get(widget._id)
|
const widgetState = widgetsState.get(widget._id)
|
||||||
@ -225,6 +224,7 @@ export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false)
|
|||||||
}
|
}
|
||||||
|
|
||||||
widgetsState.set(widget._id, {
|
widgetsState.set(widget._id, {
|
||||||
|
...widgetState,
|
||||||
_id: widget._id,
|
_id: widget._id,
|
||||||
tabs: newTabs,
|
tabs: newTabs,
|
||||||
tab: tab.id
|
tab: tab.id
|
||||||
@ -232,7 +232,9 @@ export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false)
|
|||||||
|
|
||||||
sidebarStore.set({
|
sidebarStore.set({
|
||||||
...state,
|
...state,
|
||||||
widgetsState
|
widget: widget._id,
|
||||||
|
widgetsState,
|
||||||
|
variant: SidebarVariant.EXPANDED
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -596,9 +596,6 @@ async function changeIssueDataHandler (control: TriggerControl, issueId: Ref<Iss
|
|||||||
if (data.title !== todo.title) {
|
if (data.title !== todo.title) {
|
||||||
update.title = data.title
|
update.title = data.title
|
||||||
}
|
}
|
||||||
if (data.description !== todo.description) {
|
|
||||||
update.description = data.description
|
|
||||||
}
|
|
||||||
if (data.attachedSpace !== todo.attachedSpace) {
|
if (data.attachedSpace !== todo.attachedSpace) {
|
||||||
update.attachedSpace = data.attachedSpace
|
update.attachedSpace = data.attachedSpace
|
||||||
}
|
}
|
||||||
@ -633,8 +630,8 @@ async function updateIssueHandler (tx: TxUpdateDoc<Issue>, control: TriggerContr
|
|||||||
res.push(...(await changeIssueStatusHandler(control, newStatus, tx.objectId)))
|
res.push(...(await changeIssueStatusHandler(control, newStatus, tx.objectId)))
|
||||||
}
|
}
|
||||||
const name = tx.operations.title
|
const name = tx.operations.title
|
||||||
const number = tx.operations.number
|
const space = tx.operations.space
|
||||||
if (number !== undefined || name !== undefined) {
|
if (space !== undefined || name !== undefined) {
|
||||||
res.push(...(await changeIssueDataHandler(control, tx.objectId)))
|
res.push(...(await changeIssueDataHandler(control, tx.objectId)))
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
81
tests/sanity/tests/drive/drive.spec.ts
Normal file
81
tests/sanity/tests/drive/drive.spec.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { test } from '@playwright/test'
|
||||||
|
import { PlatformSetting, PlatformURI } from '../utils'
|
||||||
|
import { Drive } from '../model/drive/types'
|
||||||
|
import { faker } from '@faker-js/faker'
|
||||||
|
import { DriveLeftMenu } from '../model/drive/drive-left-menu'
|
||||||
|
import { DriveCreateEditPopup } from '../model/drive/drive-create-edit-popup'
|
||||||
|
import { DrivesPage } from '../model/drive/drive-drives-page'
|
||||||
|
import { DriveFilesPage } from '../model/drive/drive-files-page'
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
storageState: PlatformSetting
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Drive tests', () => {
|
||||||
|
let leftMenu: DriveLeftMenu
|
||||||
|
let popupDrive: DriveCreateEditPopup
|
||||||
|
let drivesPage: DrivesPage
|
||||||
|
let filesPage: DriveFilesPage
|
||||||
|
let drive: Drive
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
popupDrive = new DriveCreateEditPopup(page)
|
||||||
|
leftMenu = new DriveLeftMenu(page)
|
||||||
|
drivesPage = new DrivesPage(page)
|
||||||
|
filesPage = new DriveFilesPage(page)
|
||||||
|
drive = {
|
||||||
|
name: faker.word.noun()
|
||||||
|
}
|
||||||
|
await (await page.goto(`${PlatformURI}/workbench/sanity-ws/drive`))?.finished()
|
||||||
|
await leftMenu.clickCreateNewDrive()
|
||||||
|
await popupDrive.createOrEditDrive(drive)
|
||||||
|
await leftMenu.clickDrives()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Check if drive exists at drives page', async () => {
|
||||||
|
await drivesPage.checkDriveExists(drive)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Create new folder, check if folder exists in drive tree', async () => {
|
||||||
|
const folder = faker.word.noun()
|
||||||
|
await drivesPage.createFolder(drive, folder)
|
||||||
|
await leftMenu.checkFolderExists(drive, folder)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Edit drive, rename drive, check if it was renamed', async () => {
|
||||||
|
const newDrive: Drive = {
|
||||||
|
name: faker.word.noun()
|
||||||
|
}
|
||||||
|
await drivesPage.clickEditDrive(drive)
|
||||||
|
await popupDrive.createOrEditDrive(newDrive)
|
||||||
|
await drivesPage.checkDriveExists(newDrive)
|
||||||
|
await drivesPage.checkDriveNotExists(drive)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Archive/unarchive drive, check drive archive status', async () => {
|
||||||
|
await drivesPage.archiveDrive(drive)
|
||||||
|
await drivesPage.checkIsArchived(drive)
|
||||||
|
await drivesPage.unarchiveDrive(drive)
|
||||||
|
await drivesPage.checkIsNotArchived(drive)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Leave/Join drive, check if user leaved/joined drive', async () => {
|
||||||
|
await drivesPage.leaveDrive(drive)
|
||||||
|
await drivesPage.checkUserNotJoinedDrive(drive)
|
||||||
|
await drivesPage.joinDrive(drive)
|
||||||
|
await drivesPage.checkUserJoinedDrive(drive)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Upload/rename/delete file, check file was uploaded, renamed and deleted', async () => {
|
||||||
|
await drivesPage.clickOnDrive(drive)
|
||||||
|
await leftMenu.clickUploadFiles()
|
||||||
|
const fileName = 'cat.jpeg'
|
||||||
|
await filesPage.uploadFile(fileName)
|
||||||
|
await filesPage.checkFileExists(fileName)
|
||||||
|
const newFileName = 'dog.jpeg'
|
||||||
|
await filesPage.renameFile(fileName, newFileName)
|
||||||
|
await filesPage.checkFileExists(newFileName)
|
||||||
|
await filesPage.deleteFile(newFileName)
|
||||||
|
await filesPage.checkFileNotExists(newFileName)
|
||||||
|
})
|
||||||
|
})
|
27
tests/sanity/tests/model/drive/drive-create-edit-popup.ts
Normal file
27
tests/sanity/tests/model/drive/drive-create-edit-popup.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { type Locator, type Page } from '@playwright/test'
|
||||||
|
import { Drive } from './types'
|
||||||
|
|
||||||
|
export class DriveCreateEditPopup {
|
||||||
|
readonly page: Page
|
||||||
|
|
||||||
|
constructor (page: Page) {
|
||||||
|
this.page = page
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly popup = (): Locator => this.page.locator('div.popup')
|
||||||
|
readonly form = (): Locator => this.popup().locator('form[id^="drive:string:"]')
|
||||||
|
readonly buttonSelectSpaceType = (): Locator => this.form().locator('div.antiGrid-row:has-text("Space type") button')
|
||||||
|
readonly inputName = (): Locator => this.form().locator('div.antiGrid-row:has-text("Name") input')
|
||||||
|
readonly inputDescription = (): Locator => this.form().locator('div.antiGrid-row:has-text("Description") input')
|
||||||
|
readonly buttonSelectOwners = (): Locator => this.form().locator('div.antiGrid-row:has-text("Owners") button')
|
||||||
|
readonly togglePrivate = (): Locator =>
|
||||||
|
this.form().locator('div.antiGrid-row:has-text("Make private") .toggle-switch')
|
||||||
|
|
||||||
|
readonly buttonSelectMembers = (): Locator => this.form().locator('div.antiGrid-row:has-text("Members") button')
|
||||||
|
readonly buttonSubmit = (): Locator => this.form().locator('button[type="submit"]')
|
||||||
|
|
||||||
|
async createOrEditDrive (drive: Drive): Promise<void> {
|
||||||
|
await this.inputName().fill(drive.name)
|
||||||
|
await this.buttonSubmit().click()
|
||||||
|
}
|
||||||
|
}
|
95
tests/sanity/tests/model/drive/drive-drives-page.ts
Normal file
95
tests/sanity/tests/model/drive/drive-drives-page.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { expect, type Locator, type Page } from '@playwright/test'
|
||||||
|
import { ButtonDrivesContextMenu, Drive } from './types'
|
||||||
|
import { CommonPage } from '../common-page'
|
||||||
|
|
||||||
|
export class DrivesPage extends CommonPage {
|
||||||
|
page: Page
|
||||||
|
|
||||||
|
constructor (page: Page) {
|
||||||
|
super(page)
|
||||||
|
this.page = page
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly cellDriveName = (driveName: string): Locator => this.page.getByRole('cell', { name: driveName }).first()
|
||||||
|
|
||||||
|
readonly buttonContextMenu = (buttonText: ButtonDrivesContextMenu): Locator =>
|
||||||
|
this.page.locator('div.antiPopup').getByRole('button', { name: buttonText })
|
||||||
|
|
||||||
|
readonly popupCreateFolder = (): Locator => this.page.locator('div.popup form[id="drive:string:CreateFolder"]')
|
||||||
|
|
||||||
|
readonly rowDrive = (driveName: string): Locator =>
|
||||||
|
this.page.locator('tbody tr').filter({
|
||||||
|
has: this.cellDriveName(driveName)
|
||||||
|
})
|
||||||
|
|
||||||
|
readonly buttonJoinDriveRow = (driveName: string): Locator =>
|
||||||
|
this.rowDrive(driveName).locator('td').nth(2).getByRole('button', { name: 'Join' })
|
||||||
|
|
||||||
|
readonly popupArchive = (): Locator => this.page.locator('.popup.endShow:text("archive")').first()
|
||||||
|
|
||||||
|
readonly cellArchiveStatusYes = (driveName: string): Locator =>
|
||||||
|
this.rowDrive(driveName).locator('td').last().locator(':text("Yes")')
|
||||||
|
|
||||||
|
async checkDriveExists (drive: Drive): Promise<void> {
|
||||||
|
await expect(this.cellDriveName(drive.name)).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkDriveNotExists (drive: Drive): Promise<void> {
|
||||||
|
await expect(this.cellDriveName(drive.name)).not.toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickButtonDriveContextMenu (drive: Drive, buttonText: ButtonDrivesContextMenu): Promise<void> {
|
||||||
|
await this.cellDriveName(drive.name).click({ button: 'right' })
|
||||||
|
await this.buttonContextMenu(buttonText).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFolder (drive: Drive, folderName: string): Promise<void> {
|
||||||
|
await this.clickButtonDriveContextMenu(drive, 'Create folder')
|
||||||
|
await this.popupCreateFolder().locator('input').fill(folderName)
|
||||||
|
await this.popupCreateFolder().getByRole('button', { name: 'Create' }).last().click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickEditDrive (drive: Drive): Promise<void> {
|
||||||
|
await this.clickButtonDriveContextMenu(drive, 'Edit drive')
|
||||||
|
}
|
||||||
|
|
||||||
|
async archiveDrive (drive: Drive): Promise<void> {
|
||||||
|
await this.clickButtonDriveContextMenu(drive, 'Archive')
|
||||||
|
await this.popupSubmitButton().click()
|
||||||
|
await this.popupArchive().waitFor({ state: 'detached' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async unarchiveDrive (drive: Drive): Promise<void> {
|
||||||
|
await this.clickButtonDriveContextMenu(drive, 'Unarchive')
|
||||||
|
await this.popupSubmitButton().click()
|
||||||
|
await this.popupArchive().waitFor({ state: 'detached' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIsArchived (drive: Drive): Promise<void> {
|
||||||
|
await expect(this.cellArchiveStatusYes(drive.name)).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIsNotArchived (drive: Drive): Promise<void> {
|
||||||
|
await expect(this.cellArchiveStatusYes(drive.name)).not.toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async leaveDrive (drive: Drive): Promise<void> {
|
||||||
|
await this.clickButtonDriveContextMenu(drive, 'Leave')
|
||||||
|
}
|
||||||
|
|
||||||
|
async joinDrive (drive: Drive): Promise<void> {
|
||||||
|
await this.clickButtonDriveContextMenu(drive, 'Join')
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkUserJoinedDrive (drive: Drive): Promise<void> {
|
||||||
|
await expect(this.buttonJoinDriveRow(drive.name)).not.toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkUserNotJoinedDrive (drive: Drive): Promise<void> {
|
||||||
|
await this.buttonJoinDriveRow(drive.name).waitFor({ state: 'visible' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickOnDrive (drive: Drive): Promise<void> {
|
||||||
|
await this.cellDriveName(drive.name).locator('a').click()
|
||||||
|
}
|
||||||
|
}
|
48
tests/sanity/tests/model/drive/drive-files-page.ts
Normal file
48
tests/sanity/tests/model/drive/drive-files-page.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { expect, type Locator, type Page } from '@playwright/test'
|
||||||
|
import { ButtonFilesContextMenu } from './types'
|
||||||
|
import { uploadFile } from '../../utils'
|
||||||
|
import { CommonPage } from '../common-page'
|
||||||
|
|
||||||
|
export class DriveFilesPage extends CommonPage {
|
||||||
|
page: Page
|
||||||
|
|
||||||
|
constructor (page: Page) {
|
||||||
|
super(page)
|
||||||
|
this.page = page
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly leftMenu = (): Locator => this.page.locator('.antiPanel-wrap__content.hulyNavPanel-container')
|
||||||
|
readonly buttonConfirmUpload = (): Locator => this.page.locator('.popup button').last()
|
||||||
|
readonly linkFile = (fileName: string): Locator => this.page.locator(`td:has(span:text-is("${fileName}")) a`).first()
|
||||||
|
readonly buttonContextMenu = (buttonText: ButtonFilesContextMenu): Locator =>
|
||||||
|
this.page.locator('div.antiPopup').getByRole('button', { name: buttonText })
|
||||||
|
|
||||||
|
async uploadFile (fileName: string): Promise<void> {
|
||||||
|
await uploadFile(this.page, fileName, 'My device')
|
||||||
|
await this.buttonConfirmUpload().click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkFileExists (fileName: string): Promise<void> {
|
||||||
|
await expect(this.linkFile(fileName)).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkFileNotExists (fileName: string): Promise<void> {
|
||||||
|
await expect(this.linkFile(fileName)).not.toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickFilesContextMenu (fileName: string, buttonText: ButtonFilesContextMenu): Promise<void> {
|
||||||
|
await this.linkFile(fileName).click({ button: 'right' })
|
||||||
|
await this.buttonContextMenu(buttonText).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async renameFile (fileName: string, newFileName: string): Promise<void> {
|
||||||
|
await this.clickFilesContextMenu(fileName, 'Rename')
|
||||||
|
await this.page.locator('.popup input').fill(newFileName)
|
||||||
|
await this.pressYesForPopup(this.page)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile (fileName: string): Promise<void> {
|
||||||
|
await this.clickFilesContextMenu(fileName, 'Delete')
|
||||||
|
await this.pressYesForPopup(this.page)
|
||||||
|
}
|
||||||
|
}
|
46
tests/sanity/tests/model/drive/drive-left-menu.ts
Normal file
46
tests/sanity/tests/model/drive/drive-left-menu.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { expect, type Locator, type Page } from '@playwright/test'
|
||||||
|
import { Drive } from './types'
|
||||||
|
|
||||||
|
export class DriveLeftMenu {
|
||||||
|
page: Page
|
||||||
|
|
||||||
|
constructor (page: Page) {
|
||||||
|
this.page = page
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly leftMenu = (): Locator => this.page.locator('.antiPanel-wrap__content.hulyNavPanel-container')
|
||||||
|
readonly buttonCreateDrive = (): Locator => this.page.locator('button#tree-drives')
|
||||||
|
readonly buttonUploadFiles = (): Locator => this.leftMenu().getByRole('button', { name: 'Upload files' })
|
||||||
|
readonly buttonDrives = (): Locator => this.page.getByRole('link', { name: 'Drives' }).getByRole('button')
|
||||||
|
readonly groupHeaderDrive = (): Locator => this.page.getByRole('button', { name: 'Drive', exact: true })
|
||||||
|
readonly treeDrives = (): Locator => this.leftMenu().locator('#navGroup-tree-drives')
|
||||||
|
readonly buttonDriveInTree = (drive: Drive): Locator =>
|
||||||
|
this.treeDrives().locator('.hulyNavGroup-container').getByRole('button', { name: drive.name })
|
||||||
|
|
||||||
|
readonly driveContainer = (drive: Drive): Locator =>
|
||||||
|
this.treeDrives()
|
||||||
|
.locator('.hulyNavGroup-container')
|
||||||
|
.filter({
|
||||||
|
has: this.page.locator(':scope').getByRole('button', { name: drive.name })
|
||||||
|
})
|
||||||
|
|
||||||
|
readonly buttonFolderInDrive = (drive: Drive, folder: string): Locator =>
|
||||||
|
this.driveContainer(drive).getByRole('button', { name: folder })
|
||||||
|
|
||||||
|
async clickCreateNewDrive (): Promise<void> {
|
||||||
|
await this.groupHeaderDrive().hover()
|
||||||
|
await this.buttonCreateDrive().click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickDrives (): Promise<void> {
|
||||||
|
await this.buttonDrives().click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkFolderExists (drive: Drive, folder: string): Promise<void> {
|
||||||
|
await expect(this.buttonFolderInDrive(drive, folder)).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickUploadFiles (): Promise<void> {
|
||||||
|
await this.buttonUploadFiles().click()
|
||||||
|
}
|
||||||
|
}
|
12
tests/sanity/tests/model/drive/types.ts
Normal file
12
tests/sanity/tests/model/drive/types.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface Drive {
|
||||||
|
spaceType?: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
owners?: string[]
|
||||||
|
private?: boolean
|
||||||
|
members?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ButtonDrivesContextMenu = 'Create folder' | 'Edit drive' | 'Archive' | 'Unarchive' | 'Leave' | 'Join'
|
||||||
|
|
||||||
|
export type ButtonFilesContextMenu = 'Rename' | 'Download' | 'Move' | 'Delete'
|
Loading…
Reference in New Issue
Block a user