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('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 50mb)', '50')
|
||||
.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 = {
|
||||
blobSizeLimitMb: parseInt(cmd.blobLimit),
|
||||
concurrency: parseInt(cmd.concurrency),
|
||||
@ -1165,6 +1166,10 @@ export function devTool (
|
||||
if (cmd.workspace !== '' && workspace.workspace !== cmd.workspace) {
|
||||
continue
|
||||
}
|
||||
if (workspace.disabled === true && !cmd.disabled) {
|
||||
console.log('ignore disabled workspace', workspace.workspace)
|
||||
continue
|
||||
}
|
||||
|
||||
console.log('start', workspace.workspace, index, '/', workspaces.length)
|
||||
await moveFiles(toolCtx, getWorkspaceId(workspace.workspace), exAdapter, params)
|
||||
@ -1182,7 +1187,8 @@ export function devTool (
|
||||
program
|
||||
.command('sync-files')
|
||||
.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()
|
||||
await withDatabase(mongodbUri, async (db) => {
|
||||
await withStorage(mongodbUri, async (adapter) => {
|
||||
@ -1196,13 +1202,22 @@ export function devTool (
|
||||
workspaces.sort((a, b) => b.lastVisit - a.lastVisit)
|
||||
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
|
||||
console.log('start', workspace.workspace, index, '/', workspaces.length)
|
||||
await syncFiles(toolCtx, getWorkspaceId(workspace.workspace), exAdapter)
|
||||
console.log('done', workspace.workspace)
|
||||
try {
|
||||
console.log('start', workspace.workspace, index, '/', workspaces.length)
|
||||
await syncFiles(toolCtx, getWorkspaceId(workspace.workspace), exAdapter)
|
||||
console.log('done', workspace.workspace)
|
||||
} catch (err) {
|
||||
console.warn('failed to sync files', err)
|
||||
}
|
||||
|
||||
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">
|
||||
import { type Attachment } from '@hcengineering/attachment'
|
||||
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
|
||||
import {
|
||||
import type { WithLookup } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import presentation, {
|
||||
FilePreviewPopup,
|
||||
canPreviewFile,
|
||||
getFileUrl,
|
||||
getPreviewAlignment,
|
||||
previewTypes
|
||||
} from '@hcengineering/presentation'
|
||||
import {
|
||||
ActionIcon,
|
||||
IconMoreH,
|
||||
IconOpen,
|
||||
Menu,
|
||||
Action as UIAction,
|
||||
closeTooltip,
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import { IconMoreH, IconOpen, Menu, Action as UIAction, closeTooltip, showPopup, tooltip } from '@hcengineering/ui'
|
||||
import view, { Action } from '@hcengineering/view'
|
||||
|
||||
import type { WithLookup } from '@hcengineering/core'
|
||||
import attachmentPlugin from '../plugin'
|
||||
import AttachmentAction from './AttachmentAction.svelte'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
import attachmentPlugin from '../plugin'
|
||||
|
||||
export let attachment: WithLookup<Attachment>
|
||||
export let isSaved = false
|
||||
@ -131,30 +123,37 @@
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
{#if canPreview}
|
||||
<AttachmentAction
|
||||
label={view.string.Open}
|
||||
icon={IconOpen}
|
||||
size="small"
|
||||
dataId="open-in-sidebar"
|
||||
action={showPreview}
|
||||
/>
|
||||
{/if}
|
||||
<a
|
||||
class="mr-1 flex-row-center gap-2 p-1"
|
||||
href={getFileUrl(attachment.file, attachment.name)}
|
||||
download={attachment.name}
|
||||
bind:this={download}
|
||||
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
|
||||
use:tooltip={{ label: presentation.string.Download }}
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{#if canPreview}
|
||||
<ActionIcon
|
||||
icon={IconOpen}
|
||||
size={'medium'}
|
||||
action={(evt) => {
|
||||
showPreview(evt)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<ActionIcon
|
||||
<AttachmentAction
|
||||
label={presentation.string.Download}
|
||||
icon={FileDownload}
|
||||
size={'medium'}
|
||||
size="small"
|
||||
dataId="open-in-sidebar"
|
||||
action={() => {
|
||||
download.click()
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
<ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} />
|
||||
<AttachmentAction
|
||||
label={view.string.MoreActions}
|
||||
icon={IconMoreH}
|
||||
size="small"
|
||||
dataId="open-in-sidebar"
|
||||
action={showMenu}
|
||||
/>
|
||||
</div>
|
||||
|
@ -26,6 +26,7 @@
|
||||
interface Dimensions {
|
||||
width: 'auto' | number
|
||||
height: 'auto' | number
|
||||
fit: 'cover' | 'contain'
|
||||
}
|
||||
|
||||
const minSizeRem = 4
|
||||
@ -45,7 +46,8 @@
|
||||
if (size === 'auto') {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
height: 'auto',
|
||||
fit: 'contain'
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,42 +57,41 @@
|
||||
if (!metadata) {
|
||||
return {
|
||||
width: preferredWidth,
|
||||
height: preferredWidth
|
||||
height: preferredWidth,
|
||||
fit: 'contain'
|
||||
}
|
||||
}
|
||||
|
||||
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 ratio = originalHeight / originalWidth
|
||||
const height = width * ratio
|
||||
|
||||
const fit = width < minSize || height < minSize ? 'cover' : 'contain'
|
||||
|
||||
if (height > maxSize) {
|
||||
return {
|
||||
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 {
|
||||
@ -110,9 +111,9 @@
|
||||
{#await getBlobRef(value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
|
||||
<img
|
||||
src={blobSrc.src}
|
||||
style:object-fit={getObjectFit(dimensions)}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
style:object-fit={dimensions.fit}
|
||||
width="100%"
|
||||
height="100%"
|
||||
srcset={blobSrc.srcset}
|
||||
alt={value.name}
|
||||
/>
|
||||
@ -124,7 +125,6 @@
|
||||
max-width: 20rem;
|
||||
max-height: 20rem;
|
||||
border-radius: 0.75rem;
|
||||
object-fit: contain;
|
||||
min-height: 4rem;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
// 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");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
@ -58,28 +58,28 @@
|
||||
}}
|
||||
>
|
||||
<AttachmentImagePreview {value} size={imageSize} />
|
||||
<div class="actions conner">
|
||||
<div class="actions">
|
||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||
</div>
|
||||
</div>
|
||||
{:else if type === 'audio'}
|
||||
<div class="buttonContainer">
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
{:else if type === 'video'}
|
||||
<div class="content buttonContainer flex-center">
|
||||
<AttachmentVideoPreview {value} preload={videoPreload} />
|
||||
<div class="actions conner">
|
||||
<div class="actions">
|
||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex buttonContainer extraWidth">
|
||||
<AttachmentPresenter {value} />
|
||||
<div class="actions conner">
|
||||
<div class="actions">
|
||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||
</div>
|
||||
</div>
|
||||
@ -100,17 +100,10 @@
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
padding: 0.125rem;
|
||||
background-color: var(--theme-comp-header-color);
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
border-radius: 0.75rem;
|
||||
|
||||
&.conner {
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0 0.4rem 0 0.25rem;
|
||||
}
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -500,9 +500,9 @@
|
||||
updateSelectedDate()
|
||||
|
||||
if (selectedMessageId !== undefined && messages.some(({ _id }) => _id === selectedMessageId)) {
|
||||
isScrollInitialized = true
|
||||
await wait()
|
||||
scrollToMessage()
|
||||
isScrollInitialized = true
|
||||
isInitialScrolling = false
|
||||
} else if (separatorIndex === -1) {
|
||||
await wait()
|
||||
@ -800,6 +800,7 @@
|
||||
bind:divBox={scrollContentBox}
|
||||
noStretch={false}
|
||||
disableOverscroll
|
||||
horizontal={false}
|
||||
onScroll={handleScroll}
|
||||
onResize={handleResize}
|
||||
>
|
||||
|
@ -143,6 +143,8 @@
|
||||
|
||||
dispatch('channel')
|
||||
}
|
||||
|
||||
$: messagesStore = dataProvider?.messagesStore
|
||||
</script>
|
||||
|
||||
{#if showHeader}
|
||||
@ -167,10 +169,13 @@
|
||||
<ThreadParentMessage {message} />
|
||||
</div>
|
||||
|
||||
{#if message.replies && message.replies > 0}
|
||||
{#if (message.replies ?? $messagesStore?.length ?? 0) > 0}
|
||||
<div class="separator">
|
||||
<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 class="line" />
|
||||
</div>
|
||||
|
@ -150,6 +150,7 @@ export async function buildThreadLink (
|
||||
loc.path[2] = chunterId
|
||||
}
|
||||
|
||||
loc.query = { message: '' }
|
||||
loc.path[3] = objectURI
|
||||
loc.path[4] = threadParent
|
||||
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) {
|
||||
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 tabName = await translate(chunter.string.ThreadIn, { name })
|
||||
const loc = getCurrentLocation()
|
||||
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 (loc.path[2] === chunterId || loc.path[2] === notificationId) {
|
||||
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) {
|
||||
sidebarStore.update((s) => {
|
||||
|
@ -11,7 +11,7 @@
|
||||
"Remove": "Удалить нотификацию",
|
||||
"RemoveAll": "Удалить все нотификации",
|
||||
"MarkAllAsRead": "Отметить все нотификации как прочитанные",
|
||||
"MarkAsRead": "Отметить нотификация прочитанной",
|
||||
"MarkAsRead": "Отметить прочитанным",
|
||||
"MarkAsUnread": "Отметить непрочитанным",
|
||||
"Archive": "Архивировать",
|
||||
"Inbox": "Входящие",
|
||||
|
@ -198,7 +198,7 @@
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => {
|
||||
dispatch('click', { context: value })
|
||||
dispatch('click', { context: value, object })
|
||||
}}
|
||||
>
|
||||
{#if isLoading}
|
||||
@ -266,7 +266,7 @@
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
dispatch('click', { context: value, notification: group[0] })
|
||||
dispatch('click', { context: value, notification: group[0], object })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -256,7 +256,7 @@
|
||||
|
||||
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> {
|
||||
@ -373,6 +373,7 @@
|
||||
}
|
||||
]
|
||||
$: $deviceInfo.replacedPanel = replacedPanel
|
||||
|
||||
onDestroy(() => {
|
||||
$deviceInfo.replacedPanel = undefined
|
||||
unsubscribeLoc()
|
||||
|
@ -583,7 +583,8 @@ export function resetInboxContext (): void {
|
||||
export async function selectInboxContext (
|
||||
linkProviders: LinkIdProvider[],
|
||||
context: DocNotifyContext,
|
||||
notification?: WithLookup<InboxNotification>
|
||||
notification?: WithLookup<InboxNotification>,
|
||||
object?: Doc
|
||||
): Promise<void> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -607,24 +608,42 @@ export async function selectInboxContext (
|
||||
const message = (notification as WithLookup<ActivityInboxNotification>)?.$lookup?.attachedTo
|
||||
|
||||
if (objectClass === chunter.class.ThreadMessage) {
|
||||
const thread = await client.findOne(
|
||||
chunter.class.ThreadMessage,
|
||||
{
|
||||
_id: objectId as Ref<ThreadMessage>
|
||||
},
|
||||
{ projection: { _id: 1, attachedTo: 1 } }
|
||||
)
|
||||
const thread =
|
||||
object?._id === objectId
|
||||
? (object as ThreadMessage)
|
||||
: await client.findOne(
|
||||
chunter.class.ThreadMessage,
|
||||
{
|
||||
_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
|
||||
}
|
||||
|
||||
if (isReactionMessage(message)) {
|
||||
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(
|
||||
linkProviders,
|
||||
objectId,
|
||||
objectClass,
|
||||
channelId,
|
||||
channelClass,
|
||||
thread as Ref<ActivityMessage>,
|
||||
objectId as Ref<ActivityMessage>
|
||||
)
|
||||
@ -633,11 +652,13 @@ export async function selectInboxContext (
|
||||
|
||||
const selectedMsg = (notification as ActivityInboxNotification)?.attachedTo
|
||||
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(
|
||||
linkProviders,
|
||||
objectId,
|
||||
objectClass,
|
||||
channelId,
|
||||
channelClass,
|
||||
thread as Ref<ActivityMessage>,
|
||||
selectedMsg ?? (objectId as Ref<ActivityMessage>)
|
||||
)
|
||||
|
@ -42,6 +42,7 @@
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"svelte": "^4.2.12",
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
"@hcengineering/analytics": "^0.6.0",
|
||||
"@hcengineering/presentation": "^0.6.3",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/view": "^0.6.13",
|
||||
|
@ -15,6 +15,7 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { type Space, type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { getFileUrl, getImageSize, imageSizeToRatio } from '@hcengineering/presentation'
|
||||
@ -116,11 +117,12 @@
|
||||
objectAttr
|
||||
})
|
||||
|
||||
let contentError = false
|
||||
let localSynced = false
|
||||
let remoteSynced = false
|
||||
|
||||
$: loading = !localSynced && !remoteSynced
|
||||
$: editable = !readonly && remoteSynced
|
||||
$: editable = !readonly && !contentError && remoteSynced
|
||||
|
||||
void localProvider.loaded.then(() => (localSynced = true))
|
||||
void remoteProvider.loaded.then(() => (remoteSynced = true))
|
||||
@ -218,7 +220,10 @@
|
||||
}
|
||||
|
||||
$: 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
|
||||
@ -364,6 +369,7 @@
|
||||
await ph
|
||||
|
||||
editor = new Editor({
|
||||
enableContentCheck: true,
|
||||
element,
|
||||
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) },
|
||||
extensions: [
|
||||
@ -432,6 +438,11 @@
|
||||
|
||||
throttle.call(updateLastUpdateTime)
|
||||
dispatch('update')
|
||||
},
|
||||
onContentError: ({ error, disableCollaboration }) => {
|
||||
disableCollaboration()
|
||||
contentError = true
|
||||
Analytics.handleError(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { EmptyMarkup, getMarkup, markupToJSON } from '@hcengineering/text'
|
||||
@ -150,6 +151,7 @@
|
||||
onMount(() => {
|
||||
void ph.then(async () => {
|
||||
editor = new Editor({
|
||||
enableContentCheck: true,
|
||||
element,
|
||||
editorProps: {
|
||||
attributes: mergeAttributes(defaultEditorAttributes, editorAttributes),
|
||||
@ -197,6 +199,9 @@
|
||||
content = getContent()
|
||||
dispatch('value', content)
|
||||
dispatch('update', content)
|
||||
},
|
||||
onContentError: ({ error }) => {
|
||||
Analytics.handleError(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -23,6 +23,7 @@
|
||||
} from '@hcengineering/text-editor'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
import { inlineToolbarKey } from './extension/inlineToolbar'
|
||||
import TextActionButton from './TextActionButton.svelte'
|
||||
@ -93,11 +94,40 @@
|
||||
$: categories.forEach((category) => {
|
||||
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>
|
||||
|
||||
<div bind:this={toolbar} style="visibility: hidden;">
|
||||
{#if editor && visible && visibleActions.length > 0}
|
||||
<div class="text-editor-toolbar buttons-group xsmall-gap mb-4">
|
||||
<div bind:this={toolbar} class="p-2" style="visibility: hidden;">
|
||||
{#if editor && visible && !selecting && visibleActions.length > 0}
|
||||
<div class="text-editor-toolbar buttons-group xsmall-gap">
|
||||
{#each Object.values(categories) as category, index}
|
||||
{#if index > 0}
|
||||
<div class="buttons-divider" />
|
||||
@ -113,8 +143,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
.text-editor-toolbar {
|
||||
margin: -0.5rem -0.25rem 0.5rem;
|
||||
padding: 0.375rem;
|
||||
padding: 0.25rem;
|
||||
background-color: var(--theme-comp-header-color);
|
||||
border-radius: 0.5rem;
|
||||
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 { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
|
||||
import { common, createLowlight } from 'lowlight'
|
||||
import { isChangeEditable } from './editable'
|
||||
|
||||
type Lowlight = ReturnType<typeof createLowlight>
|
||||
|
||||
@ -117,7 +118,7 @@ export function LanguageSelector (options: CodeBlockLowlightOptions): Plugin {
|
||||
return createDecorations(state.doc, options)
|
||||
},
|
||||
apply (tr, prev) {
|
||||
if (tr.docChanged) {
|
||||
if (tr.docChanged || isChangeEditable(tr)) {
|
||||
return createDecorations(tr.doc, options)
|
||||
}
|
||||
|
||||
@ -133,7 +134,7 @@ function createDecorations (doc: ProseMirrorNode, options: CodeBlockLowlightOpti
|
||||
doc.descendants((node, pos) => {
|
||||
if (node.type.name === CodeBlockLowlight.name) {
|
||||
decorations.push(
|
||||
Decoration.widget(pos + node.nodeSize - 1, (view) => {
|
||||
Decoration.widget(pos + 1, (view) => {
|
||||
const button = createLangButton(node.attrs.language)
|
||||
|
||||
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>({
|
||||
name: 'focus',
|
||||
addStorage () {
|
||||
return { canBlur: true }
|
||||
},
|
||||
|
@ -6,6 +6,7 @@ export interface SubmitOptions {
|
||||
}
|
||||
|
||||
export const SubmitExtension = Extension.create<SubmitOptions>({
|
||||
name: 'submit',
|
||||
addKeyboardShortcuts () {
|
||||
const shortcuts: Record<string, KeyboardShortcutCommand> = {
|
||||
Space: () => {
|
||||
|
@ -23,6 +23,7 @@ import ListKeymap from '@tiptap/extension-list-keymap'
|
||||
import TableHeader from '@tiptap/extension-table-header'
|
||||
import 'prosemirror-codemark/dist/codemark.css'
|
||||
|
||||
import { EditableExtension } from '../components/extension/editable'
|
||||
import { CodeBlockHighlighExtension, codeBlockHighlightOptions } from '../components/extension/codeblock'
|
||||
import { NoteExtension, type NoteOptions } from '../components/extension/note'
|
||||
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)],
|
||||
[210, CodeExtension.configure(codeOptions)],
|
||||
[220, HardBreakExtension.configure({ shortcuts: mode })]
|
||||
|
@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
import { type Doc as YDoc } from 'yjs'
|
||||
import { IndexeddbPersistence } from 'y-indexeddb'
|
||||
@ -26,11 +24,7 @@ export class IndexeddbProvider extends IndexeddbPersistence implements Provider
|
||||
readonly awareness: Awareness | null = null
|
||||
|
||||
constructor (documentId: string, doc: YDoc) {
|
||||
const workspaceId: string = getMetadata(presentation.metadata.WorkspaceId) ?? ''
|
||||
|
||||
const name = `${workspaceId}/${documentId}`
|
||||
|
||||
super(name, doc)
|
||||
super(documentId, doc)
|
||||
|
||||
this.loaded = new Promise((resolve) => {
|
||||
this.on('synced', resolve)
|
||||
|
@ -28,7 +28,7 @@
|
||||
$: if (query) updateSearchQuery(search)
|
||||
let resultQuery: DocumentQuery<Issue> = { ...searchQuery }
|
||||
|
||||
$: if (!label && title) {
|
||||
$: if (title) {
|
||||
void translate(title, {}, $themeStore.language).then((res) => {
|
||||
label = res
|
||||
})
|
||||
|
@ -24,7 +24,7 @@
|
||||
Breadcrumbs,
|
||||
getCurrentLocation
|
||||
} from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
import { closeWidgetTab, sidebarStore, SidebarVariant, WidgetState, openWidgetTab, closeWidget } from '../../sidebar'
|
||||
import WidgetsBar from './widgets/WidgetsBar.svelte'
|
||||
@ -46,36 +46,15 @@
|
||||
$: widgetState = widget !== undefined ? $sidebarStore.widgetsState.get(widget._id) : undefined
|
||||
|
||||
$: tabId = widgetState?.tab
|
||||
$: tabs = getTabs(widget, widgetState)
|
||||
$: tabs = widgetState?.tabs ?? []
|
||||
$: tab = tabId !== undefined ? tabs.find((it) => it.id === tabId) ?? tabs[0] : tabs[0]
|
||||
|
||||
$: if ($sidebarStore.widget === undefined) {
|
||||
sidebarStore.update((s) => ({ ...s, variant: SidebarVariant.MINI }))
|
||||
}
|
||||
|
||||
function getTabs (widget?: Widget, state?: WidgetState): WidgetTab[] {
|
||||
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) => {
|
||||
function closeWrongTabs (loc: Location): void {
|
||||
if (widget === undefined) return
|
||||
|
||||
for (const tab of tabs) {
|
||||
if (tab.allowedPath !== undefined && !tab.isPinned) {
|
||||
const path = loc.path.join('/')
|
||||
@ -84,6 +63,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = locationStore.subscribe((loc: Location) => {
|
||||
closeWrongTabs(loc)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
closeWrongTabs(getCurrentLocation())
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
|
@ -203,7 +203,6 @@ export function openWidgetTab (widget: Ref<Widget>, tab: string): void {
|
||||
}
|
||||
|
||||
export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false): void {
|
||||
openWidget(widget)
|
||||
const state = get(sidebarStore)
|
||||
const { widgetsState } = state
|
||||
const widgetState = widgetsState.get(widget._id)
|
||||
@ -225,6 +224,7 @@ export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false)
|
||||
}
|
||||
|
||||
widgetsState.set(widget._id, {
|
||||
...widgetState,
|
||||
_id: widget._id,
|
||||
tabs: newTabs,
|
||||
tab: tab.id
|
||||
@ -232,7 +232,9 @@ export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false)
|
||||
|
||||
sidebarStore.set({
|
||||
...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) {
|
||||
update.title = data.title
|
||||
}
|
||||
if (data.description !== todo.description) {
|
||||
update.description = data.description
|
||||
}
|
||||
if (data.attachedSpace !== todo.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)))
|
||||
}
|
||||
const name = tx.operations.title
|
||||
const number = tx.operations.number
|
||||
if (number !== undefined || name !== undefined) {
|
||||
const space = tx.operations.space
|
||||
if (space !== undefined || name !== undefined) {
|
||||
res.push(...(await changeIssueDataHandler(control, tx.objectId)))
|
||||
}
|
||||
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