Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-10-04 00:37:36 +07:00
commit 17912ea4ed
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
62 changed files with 732 additions and 133 deletions

View File

@ -9,12 +9,19 @@ then
c=( ${a[2]//[^0-9]*/ } )
((c++))
version="${a[0]}.${a[1]}.${c}-staging"
echo "Tagging stating $1 with version ${version}"
echo "Tagging staging $1 with version ${version}"
docker tag "$1:$rev_version" "$1:$version"
for n in {1..5}; do
docker push "$1:$version" && break
echo 'Docker failed to push, wait 5 seconds'
sleep 5
if (( $n < 5 ))
then
echo 'Docker failed to push, wait 5 second'
sleep 5
else
echo '5 push attempts failed, exiting with failure'
exit 1
fi
done
else
echo "Tagging release $1 with version ${version}"
@ -22,12 +29,26 @@ else
docker tag "$1:$rev_version" "$1:latest"
for n in {1..5}; do
docker push "$1:$version" && break
echo 'Docker failed to push, wait 5 seconds'
sleep 5
if (( $n < 5 ))
then
echo 'Docker failed to push, wait 5 second'
sleep 5
else
echo '5 push attempts failed, exiting with failure'
exit 1
fi
done
for n in {1..5}; do
docker push "$1:latest" && break
echo 'Docker failed to push, wait 5 seconds'
sleep 5
if (( $n < 5 ))
then
echo 'Docker failed to push, wait 5 second'
sleep 5
else
echo '5 push attempts failed, exiting with failure'
exit 1
fi
done
fi

View File

@ -140,6 +140,7 @@ export async function moveAccountDbFromMongoToPG (
mongoDb: AccountDB,
pgDb: AccountDB
): Promise<void> {
// [accountId, workspaceId]
const workspaceAssignments: [ObjectId, ObjectId][] = []
const accounts = await listAccounts(mongoDb)
const workspaces = await listWorkspacesPure(mongoDb)
@ -153,14 +154,26 @@ export async function moveAccountDbFromMongoToPG (
delete (pgAccount as any).workspaces
if (pgAccount.createdOn == null) {
pgAccount.createdOn = Date.now()
}
if (pgAccount.first == null) {
pgAccount.first = 'NotSet'
}
if (pgAccount.last == null) {
pgAccount.last = 'NotSet'
}
for (const workspaceString of new Set(mongoAccount.workspaces.map((w) => w.toString()))) {
workspaceAssignments.push([pgAccount._id, workspaceString])
}
const exists = await getAccount(pgDb, pgAccount.email)
if (exists === null) {
await pgDb.account.insertOne(pgAccount)
ctx.info('Moved account', { email: pgAccount.email })
for (const workspace of mongoAccount.workspaces) {
workspaceAssignments.push([pgAccount._id, workspace.toString()])
}
}
}
@ -170,6 +183,10 @@ export async function moveAccountDbFromMongoToPG (
_id: mongoWorkspace._id.toString()
}
if (pgWorkspace.createdOn == null) {
pgWorkspace.createdOn = Date.now()
}
// delete deprecated fields
delete (pgWorkspace as any).createProgress
delete (pgWorkspace as any).creating
@ -202,9 +219,19 @@ export async function moveAccountDbFromMongoToPG (
}
}
if (workspaceAssignments.length > 0) {
for (const [accountId, workspaceId] of workspaceAssignments) {
await pgDb.assignWorkspace(accountId, workspaceId)
}
const pgAssignments = (await listAccounts(pgDb)).reduce<Record<ObjectId, ObjectId[]>>((assignments, acc) => {
assignments[acc._id] = acc.workspaces
return assignments
}, {})
const assignmentsToInsert = workspaceAssignments.filter(
([accountId, workspaceId]) =>
pgAssignments[accountId] === undefined || !pgAssignments[accountId].includes(workspaceId)
)
for (const [accountId, workspaceId] of assignmentsToInsert) {
await pgDb.assignWorkspace(accountId, workspaceId)
}
ctx.info('Assignments made', { count: assignmentsToInsert.length })
}

View File

@ -51,6 +51,7 @@ import { documentsOperation } from '@hcengineering/model-controlled-documents'
import { productsOperation } from '@hcengineering/model-products'
import { requestOperation } from '@hcengineering/model-request'
import { analyticsCollectorOperation } from '@hcengineering/model-analytics-collector'
import { workbenchOperation } from '@hcengineering/model-workbench'
export const migrateOperations: [string, MigrateOperation][] = [
['core', coreOperation],
@ -90,5 +91,6 @@ export const migrateOperations: [string, MigrateOperation][] = [
['textEditorOperation', textEditorOperation],
// We should call notification migration after activityServer and chunter
['notification', notificationOperation],
['analyticsCollector', analyticsCollectorOperation]
['analyticsCollector', analyticsCollectorOperation],
['workbench', workbenchOperation]
]

View File

@ -40,6 +40,7 @@
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-uploader": "^0.6.0"
"@hcengineering/model-uploader": "^0.6.0",
"@hcengineering/workbench": "^0.6.16"
}
}

View File

@ -31,6 +31,8 @@ import {
import core, { TAttachedDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view'
import workbench, { WidgetType } from '@hcengineering/workbench'
import presentation from '@hcengineering/model-presentation'
import attachment from './plugin'
@ -97,6 +99,24 @@ export function createModel (builder: Builder): void {
editor: attachment.component.Photos
})
builder.createDoc(
workbench.class.Widget,
core.space.Model,
{
label: attachment.string.Files,
type: WidgetType.Flexible,
icon: attachment.icon.Attachment,
component: attachment.component.PreviewWidget,
closeIfNoTabs: true
},
attachment.ids.PreviewWidget
)
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: presentation.extension.FilePreviewPopupActions,
component: attachment.component.PreviewPopupActions
})
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,

View File

@ -24,7 +24,9 @@ import type { ActionCategory } from '@hcengineering/view'
export default mergeIds(attachmentId, attachment, {
component: {
AttachmentPresenter: '' as AnyComponent
AttachmentPresenter: '' as AnyComponent,
PreviewWidget: '' as AnyComponent,
PreviewPopupActions: '' as AnyComponent
},
string: {
AddAttachment: '' as IntlString,

View File

@ -388,7 +388,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
label: notification.string.Inbox,
icon: notification.icon.Inbox,
icon: notification.icon.Notifications,
alias: notificationId,
hidden: true,
locationResolver: notification.resolver.Location,

View File

@ -35,6 +35,7 @@
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/preference": "^0.6.13",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/workbench": "^0.6.16",

View File

@ -40,6 +40,7 @@ import presentation from '@hcengineering/model-presentation'
import workbench from './plugin'
export { workbenchId } from '@hcengineering/workbench'
export { workbenchOperation } from './migration'
export type { Application }
@Model(workbench.class.Application, core.class.Doc, DOMAIN_MODEL)

View File

@ -0,0 +1,40 @@
//
// 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.
//
import {
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
tryMigrate
} from '@hcengineering/model'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import workbench from '@hcengineering/workbench'
import { workbenchId } from '.'
async function removeTabs (client: MigrationClient): Promise<void> {
await client.deleteMany(DOMAIN_PREFERENCE, { _class: workbench.class.WorkbenchTab })
}
export const workbenchOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, workbenchId, [
{
state: 'remove-wrong-tabs-v1',
func: removeTabs
}
])
},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {}
}

View File

@ -0,0 +1,43 @@
<!--
// 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 { Ref, Blob } from '@hcengineering/core'
import { Button } from '@hcengineering/ui'
import { getFileUrl } from '../file'
import Download from './icons/Download.svelte'
import presentation from '../plugin'
export let file: Ref<Blob> | undefined
export let name: string
let download: HTMLAnchorElement
$: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
</script>
{#await srcRef then src}
{#if src !== ''}
<a class="no-line" href={src} download={name} bind:this={download}>
<Button
icon={Download}
kind={'icon'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
{/if}
{/await}

View File

@ -15,17 +15,17 @@
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Button, Dialog, tooltip } from '@hcengineering/ui'
import { Dialog, tooltip } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import presentation from '../plugin'
import { getFileUrl } from '../file'
import { BlobMetadata } from '../types'
import ActionContext from './ActionContext.svelte'
import FilePreview from './FilePreview.svelte'
import Download from './icons/Download.svelte'
import DownloadFileButton from './DownloadFileButton.svelte'
import { ComponentExtensions } from '../index'
import presentation from '../plugin'
import FileTypeIcon from './FileTypeIcon.svelte'
export let file: Ref<Blob> | undefined
export let name: string
@ -38,21 +38,11 @@
const dispatch = createEventDispatcher()
let download: HTMLAnchorElement
onMount(() => {
if (fullSize) {
dispatch('fullsize')
}
})
function iconLabel (name: string): string {
const parts = `${name}`.split('.')
const ext = parts[parts.length - 1]
return ext.substring(0, 4).toUpperCase()
}
$: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
</script>
<ActionContext context={{ mode: 'browser' }} />
@ -67,9 +57,7 @@
<div class="antiTitle icon-wrapper">
{#if showIcon}
<div class="wrapped-icon">
<div class="flex-center icon">
{iconLabel(name)}
</div>
<FileTypeIcon {name} />
</div>
{/if}
<span class="wrapped-title" use:tooltip={{ label: getEmbeddedLabel(name) }}>{name}</span>
@ -77,39 +65,19 @@
</svelte:fragment>
<svelte:fragment slot="utils">
{#await srcRef then src}
{#if src !== ''}
<a class="no-line" href={src} download={name} bind:this={download}>
<Button
icon={Download}
kind={'ghost'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
{/if}
{/await}
<DownloadFileButton {name} {file} />
<ComponentExtensions
extension={presentation.extension.FilePreviewPopupActions}
props={{
file,
name,
contentType,
metadata
}}
/>
</svelte:fragment>
{#if file}
<FilePreview {file} {contentType} {name} {metadata} {props} fit />
{/if}
</Dialog>
<style lang="scss">
.icon {
position: relative;
flex-shrink: 0;
width: 2rem;
height: 2rem;
font-weight: 500;
font-size: 0.625rem;
color: var(--primary-button-color);
background-color: var(--primary-button-default);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,42 @@
<!--
// 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">
export let name: string
function iconLabel (name: string): string {
const parts = `${name}`.split('.')
const ext = parts[parts.length - 1]
return ext.substring(0, 4).toUpperCase()
}
</script>
<div class="flex-center icon">
{iconLabel(name)}
</div>
<style lang="scss">
.icon {
position: relative;
flex-shrink: 0;
width: 2rem;
height: 2rem;
font-weight: 500;
font-size: 0.625rem;
color: var(--primary-button-color);
background-color: var(--primary-button-default);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
}
</style>

View File

@ -0,0 +1,26 @@
<!--
// 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 { markupToJSON } from '@hcengineering/text'
import LiteNode from './markup/lite/LiteNode.svelte'
export let message: string
$: node = markupToJSON(message)
</script>
<div class="text-markup-view">
<LiteNode {node} />
</div>

View File

@ -0,0 +1,30 @@
<!--
// 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 { MarkupNode } from '@hcengineering/text'
import LiteNodeContent from './LiteNodeContent.svelte'
import NodeMarks from '../NodeMarks.svelte'
export let node: MarkupNode
</script>
{#if node}
{@const marks = node.marks ?? []}
<NodeMarks {marks}>
<LiteNodeContent {node} />
</NodeMarks>
{/if}

View File

@ -0,0 +1,105 @@
<!--
// 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 { Class, Doc, Ref } from '@hcengineering/core'
import { AttrValue, MarkupNode, MarkupNodeType, MarkupMarkType } from '@hcengineering/text'
import LiteNodes from './LiteNodes.svelte'
import ObjectNode from '../ObjectNode.svelte'
import NodeMarks from '../NodeMarks.svelte'
export let node: MarkupNode
function toRef (objectId: string): Ref<Doc> {
return objectId as Ref<Doc>
}
function toClassRef (objectClass: string): Ref<Class<Doc>> {
if (objectClass === 'contact:class:Employee') {
return 'contact:mixin:Employee' as Ref<Class<Doc>>
}
return objectClass as Ref<Class<Doc>>
}
function toString (value: AttrValue | undefined): string | undefined {
return value !== undefined ? `${value}` : undefined
}
function toNumber (value: AttrValue | undefined): number | undefined {
if (typeof value === 'boolean') {
return value ? 1 : 0
}
return value !== undefined ? (typeof value === 'string' ? parseInt(value) : value) : undefined
}
</script>
{#if node}
{@const attrs = node.attrs ?? {}}
{@const nodes = node.content ?? []}
{#if node.type === MarkupNodeType.doc}
<LiteNodes {nodes} />
{:else if node.type === MarkupNodeType.text}
{node.text}
{:else if node.type === MarkupNodeType.paragraph}
<p class="p-inline contrast" class:overflow-label={true} style:margin="0">
<LiteNodes {nodes} />
</p>
{:else if node.type === MarkupNodeType.blockquote}
<LiteNodes {nodes} />
{:else if node.type === MarkupNodeType.horizontal_rule}
<!-- nothing-->
{:else if node.type === MarkupNodeType.heading}
{@const level = toNumber(node.attrs?.level) ?? 1}
{@const element = `h${level}`}
<svelte:element this={element}>
<LiteNodes {nodes} />
</svelte:element>
{:else if node.type === MarkupNodeType.code_block}
<p class="p-inline contrast" class:overflow-label={true} style:margin="0">
<NodeMarks
marks={[
{
type: MarkupMarkType.code,
attrs: {}
}
]}
>
<LiteNodes {nodes} />
</NodeMarks>
</p>
{:else if node.type === MarkupNodeType.reference}
{@const objectId = toString(attrs.id)}
{@const objectClass = toString(attrs.objectclass)}
{@const objectLabel = toString(attrs.label)}
{#if objectClass !== undefined && objectId !== undefined}
<ObjectNode _id={toRef(objectId)} _class={toClassRef(objectClass)} title={objectLabel} />
{:else}
<LiteNodes {nodes} />
{/if}
{:else if node.type === MarkupNodeType.taskList}
<!-- TODO not implemented -->
{:else if node.type === MarkupNodeType.taskItem}
<!-- TODO not implemented -->
{:else if node.type === MarkupNodeType.subLink}
<sub>
<LiteNodes {nodes} />
</sub>
{:else}
<LiteNodes {nodes} />
{/if}
{/if}

View File

@ -0,0 +1,27 @@
<!--
// 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 { MarkupNode } from '@hcengineering/text'
import LiteNode from './LiteNode.svelte'
export let nodes: MarkupNode[]
</script>
{#if nodes}
{#each nodes as node}
<LiteNode {node} />
{/each}
{/if}

View File

@ -47,6 +47,9 @@ export { default as BreadcrumbsElement } from './components/breadcrumbs/Breadcru
export { default as ComponentExtensions } from './components/extensions/ComponentExtensions.svelte'
export { default as DocCreateExtComponent } from './components/extensions/DocCreateExtComponent.svelte'
export { default as SearchResult } from './components/SearchResult.svelte'
export { default as LiteMessageViewer } from './components/LiteMessageViewer.svelte'
export { default as DownloadFileButton } from './components/DownloadFileButton.svelte'
export { default as FileTypeIcon } from './components/FileTypeIcon.svelte'
export { default } from './plugin'
export * from './types'
export * from './utils'

View File

@ -123,7 +123,8 @@ export default plugin(presentationId, {
ContentTypeNotSupported: '' as IntlString
},
extension: {
FilePreviewExtension: '' as ComponentExtensionId
FilePreviewExtension: '' as ComponentExtensionId,
FilePreviewPopupActions: '' as ComponentExtensionId
},
metadata: {
ModelVersion: '' as Metadata<string>,

View File

@ -108,7 +108,7 @@ li {
}
p {
user-select: text;
user-select:inherit;
a {
word-break: break-all;

View File

@ -22,6 +22,7 @@
export let icon: Asset | AnySvelteComponent | ComponentType | undefined = undefined
export let iconProps: any | undefined = undefined
export let iconWidth: string | undefined = undefined
export let iconMargin: string | undefined = undefined
export let withoutIconBackground = false
export let label: IntlString | undefined = undefined
export let title: string | undefined = undefined
@ -31,7 +32,12 @@
<button class="hulyBreadcrumb-container {size}" class:current={isCurrent} on:click>
{#if size === 'large' && icon}
<div class="hulyBreadcrumb-avatar" style:width={iconWidth ?? null} class:withoutIconBackground>
<div
class="hulyBreadcrumb-avatar"
style:width={iconWidth ?? null}
style:margin={iconMargin}
class:withoutIconBackground
>
<Icon {icon} size={'small'} {iconProps} />
</div>
{/if}

View File

@ -166,7 +166,7 @@ export const settingsSeparators: DefSeparators = [
export const mainSeparators: DefSeparators = [
{ minSize: 30, size: 'auto', maxSize: 'auto' },
{ minSize: 20, size: 30, maxSize: 45, float: 'sidebar' }
{ minSize: 20, size: 30, maxSize: 80, float: 'sidebar' }
]
export const secondNavSeparators: DefSeparators = [{ minSize: 7, size: 7.5, maxSize: 15, float: 'navigator' }, null]

View File

@ -130,6 +130,7 @@ export interface BreadcrumbItem {
icon?: Asset | AnySvelteComponent | ComponentType
iconProps?: any
iconWidth?: string
iconMargin?: string
withoutIconBackground?: boolean
label?: IntlString
title?: string

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { getClient, MessageViewer } from '@hcengineering/presentation'
import { getClient, LiteMessageViewer } from '@hcengineering/presentation'
import { Person, type PersonAccount } from '@hcengineering/contact'
import {
Avatar,
@ -150,7 +150,7 @@
<Label label={intlLabel} />
{/if}
{#if text}
<MessageViewer message={text} preview />
<LiteMessageViewer message={text} />
{/if}
</span>
{/if}

View File

@ -289,6 +289,7 @@
border: 1px solid transparent;
border-radius: 0.25rem;
width: 100%;
user-select: text;
&.clickable {
cursor: pointer;

View File

@ -52,6 +52,7 @@
"UnPinAttachment": "Mark less important",
"FilterAttachments": "Attachments",
"RemovedAttachment": "Removed attachment",
"ContentType": "Content type"
"ContentType": "Content type",
"OpenInWindow": "Open in window"
}
}

View File

@ -52,6 +52,7 @@
"UnPinAttachment": "Marcar como menos importante",
"FilterAttachments": "Adjuntos",
"RemovedAttachment": "Adjunto eliminado",
"ContentType": "Tipo de contenido"
"ContentType": "Tipo de contenido",
"OpenInWindow": "Abrir en ventana"
}
}

View File

@ -52,6 +52,7 @@
"UnPinAttachment": "Marquer comme moins important",
"FilterAttachments": "Pièces jointes",
"RemovedAttachment": "Pièce jointe supprimée",
"ContentType": "Type de contenu"
"ContentType": "Type de contenu",
"OpenInWindow": "Ouvrir dans une fenêtre"
}
}

View File

@ -52,6 +52,7 @@
"UnPinAttachment": "Marcar como menos importante",
"FilterAttachments": "Anexos",
"RemovedAttachment": "Anexo removido",
"ContentType": "Tipo de conteúdo"
"ContentType": "Tipo de conteúdo",
"OpenInWindow": "Abrir numa janela"
}
}

View File

@ -52,6 +52,7 @@
"UnPinAttachment": "Убрать пометку важное",
"FilterAttachments": "Вложения",
"RemovedAttachment": "Удалил(а) вложение",
"ContentType": "Тип контента"
"ContentType": "Тип контента",
"OpenInWindow": "Открыть в окне"
}
}

View File

@ -52,6 +52,7 @@
"UnPinAttachment": "标记为不重要",
"FilterAttachments": "附件",
"RemovedAttachment": "移除附件",
"ContentType": "内容类型"
"ContentType": "内容类型",
"OpenInWindow": "在新窗口中打开"
}
}

View File

@ -38,8 +38,8 @@
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/attachment": "^0.6.14",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32",
@ -55,6 +55,7 @@
"@hcengineering/uploader": "^0.6.0",
"@hcengineering/view": "^0.6.13",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/workbench": "^0.6.16",
"filesize": "^8.0.3",
"svelte": "^4.2.12"
},

View File

@ -25,10 +25,12 @@
} from '@hcengineering/presentation'
import { IconMoreH, Menu, Action as UIAction, closeTooltip, showPopup, tooltip } from '@hcengineering/ui'
import view, { Action } from '@hcengineering/view'
import workbench from '@hcengineering/workbench'
import AttachmentAction from './AttachmentAction.svelte'
import FileDownload from './icons/FileDownload.svelte'
import attachmentPlugin from '../plugin'
import { openAttachmentInSidebar } from '../utils'
export let attachment: WithLookup<Attachment>
export let isSaved = false
@ -95,6 +97,13 @@
if (canPreview) {
actions.push(openAction)
}
actions.push({
icon: view.icon.DetailsFilled,
label: workbench.string.OpenInSidebar,
action: async () => {
await openAttachmentInSidebar(attachment)
}
})
actions.push({
label: saveAttachmentAction.label,
icon: saveAttachmentAction.icon,

View File

@ -17,11 +17,10 @@
import type { Attachment } from '@hcengineering/attachment'
import core, { type WithLookup } from '@hcengineering/core'
import presentation, {
FilePreviewPopup,
canPreviewFile,
FilePreviewPopup,
getBlobRef,
getFileUrl,
getPreviewAlignment,
previewTypes,
sizeToWidth
} from '@hcengineering/presentation'
@ -29,7 +28,7 @@
import { permissionsStore } from '@hcengineering/view-resources'
import filesize from 'filesize'
import { createEventDispatcher } from 'svelte'
import { getType } from '../utils'
import { getType, openAttachmentInSidebar } from '../utils'
import AttachmentName from './AttachmentName.svelte'
@ -70,7 +69,7 @@
canPreview = false
}
function clickHandler (e: MouseEvent): void {
async function clickHandler (e: MouseEvent): Promise<void> {
if (value === undefined || !canPreview) return
e.preventDefault()
@ -80,16 +79,20 @@
return
}
closeTooltip()
showPopup(
FilePreviewPopup,
{
file: value.file,
contentType: value.type,
name: value.name,
metadata: value.metadata
},
getPreviewAlignment(value.type)
)
if (value.type.startsWith('image/') || value.type.startsWith('video/') || value.type.startsWith('audio/')) {
showPopup(
FilePreviewPopup,
{
file: value.file,
contentType: value.type,
name: value.name,
metadata: value.metadata
},
'centered'
)
} else {
await openAttachmentInSidebar(value)
}
}
function middleClickHandler (e: MouseEvent): void {

View File

@ -14,13 +14,13 @@
// limitations under the License.
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import { Attachment } from '@hcengineering/attachment'
import { FilePreviewPopup } from '@hcengineering/presentation'
import { closeTooltip, showPopup } from '@hcengineering/ui'
import { ListSelectionProvider } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { WithLookup } from '@hcengineering/core'
import type { WithLookup } from '@hcengineering/core'
import { AttachmentImageSize } from '../types'
import { getType } from '../utils'
import AttachmentActions from './AttachmentActions.svelte'

View File

@ -0,0 +1,41 @@
<!--
// 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 { Blob, Ref } from '@hcengineering/core'
import { BlobMetadata } from '@hcengineering/presentation'
import { Button, closePopup, closeTooltip, IconDetailsFilled } from '@hcengineering/ui'
import workbench from '@hcengineering/workbench'
import { openFilePreviewInSidebar } from '../utils'
export let file: Ref<Blob> | undefined
export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined
</script>
{#if file}
<Button
icon={IconDetailsFilled}
kind="icon"
on:click={() => {
if (file === undefined) return
closeTooltip()
closePopup()
void openFilePreviewInSidebar(file, name, contentType, metadata)
}}
showTooltip={{ label: workbench.string.OpenInSidebar }}
/>
{/if}

View File

@ -0,0 +1,68 @@
<!--
// 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 workbench, { Widget, WidgetTab } from '@hcengineering/workbench'
import { FilePreview, DownloadFileButton, FilePreviewPopup, FileTypeIcon } from '@hcengineering/presentation'
import { Breadcrumbs, Button, closeTooltip, Header, IconOpen, showPopup } from '@hcengineering/ui'
import { getResource } from '@hcengineering/platform'
import view from '@hcengineering/view'
import attachment from '../plugin'
export let widget: Widget
export let tab: WidgetTab
$: file = tab.data?.file
$: fileName = tab.data?.name ?? ''
$: contentType = tab.data?.contentType
$: metadata = tab.data?.metadata
async function closeTab (): Promise<void> {
const fn = await getResource(workbench.function.CloseWidgetTab)
await fn(widget, tab.id)
}
</script>
<Header
allowFullsize={false}
type="type-aside"
hideBefore={true}
hideActions={false}
hideDescription={true}
adaptive="disabled"
closeOnEscape={false}
on:close={closeTab}
>
<Breadcrumbs
items={[{ title: fileName, icon: FileTypeIcon, iconProps: { name: fileName }, iconMargin: '0 0.5rem 0 0' }]}
currentOnly
/>
<svelte:fragment slot="actions">
<DownloadFileButton name={fileName} {file} />
<Button
icon={view.icon.Open}
kind="icon"
showTooltip={{ label: attachment.string.OpenInWindow }}
on:click={() => {
closeTooltip()
showPopup(FilePreviewPopup, { file, name: fileName, contentType, metadata }, 'centered')
}}
/>
</svelte:fragment>
</Header>
{#if file}
<FilePreview {file} {contentType} name={fileName} {metadata} />
{/if}

View File

@ -40,6 +40,8 @@ import AttachmentsUpdatedMessage from './components/activity/AttachmentsUpdatedM
import IconAttachment from './components/icons/Attachment.svelte'
import FileDownload from './components/icons/FileDownload.svelte'
import IconUploadDuo from './components/icons/UploadDuo.svelte'
import PreviewWidget from './components/PreviewWidget.svelte'
import PreviewPopupActions from './components/PreviewPopupActions.svelte'
export * from './types'
@ -256,7 +258,9 @@ export default async (): Promise<Resources> => ({
Attachments,
FileBrowser,
Photos,
PDFViewer
PDFViewer,
PreviewWidget,
PreviewPopupActions
},
activity: {
AttachmentsUpdatedMessage

View File

@ -14,7 +14,7 @@
// limitations under the License.
//
import { type Attachment } from '@hcengineering/attachment'
import { type BlobMetadata, type Attachment } from '@hcengineering/attachment'
import {
type Blob,
type Class,
@ -24,8 +24,10 @@ import {
type Ref,
type Space
} from '@hcengineering/core'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { type FileOrBlob, getFileMetadata, uploadFile } from '@hcengineering/presentation'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { type FileOrBlob, getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation'
import workbench, { type WidgetTab } from '@hcengineering/workbench'
import view from '@hcengineering/view'
import attachment from './plugin'
@ -98,3 +100,38 @@ export function getType (type: string): 'image' | 'text' | 'json' | 'video' | 'a
return 'other'
}
export async function openAttachmentInSidebar (value: Attachment): Promise<void> {
await openFilePreviewInSidebar(value.file, value.name, value.type, value.metadata)
}
export async function openFilePreviewInSidebar (
file: Ref<Blob>,
name: string,
contentType: string,
metadata?: BlobMetadata
): Promise<void> {
const client = getClient()
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: attachment.ids.PreviewWidget })[0]
const createFn = await getResource(workbench.function.CreateWidgetTab)
let icon = attachment.icon.Attachment
if (contentType.startsWith('image/')) {
icon = view.icon.Image
} else if (contentType.startsWith('video/')) {
icon = view.icon.Video
} else if (contentType.startsWith('audio/')) {
icon = view.icon.Audio
} else {
icon = view.icon.File
}
const tab: WidgetTab = {
id: file,
icon,
name,
widget: attachment.ids.PreviewWidget,
data: { file, name, contentType, metadata }
}
await createFn(widget, tab, true)
}

View File

@ -40,6 +40,7 @@
"@hcengineering/platform": "^0.6.11",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/core": "^0.6.32",
"@hcengineering/workbench": "^0.6.16",
"@hcengineering/preference": "^0.6.13"
},
"repository": "https://github.com/hcengineering/platform",

View File

@ -19,6 +19,7 @@ import type { Asset, Plugin } from '@hcengineering/platform'
import { IntlString, plugin, Resource } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
import { AnyComponent } from '@hcengineering/ui'
import { Widget } from '@hcengineering/workbench'
export * from './analytics'
@ -87,6 +88,9 @@ export default plugin(attachmentId, {
UploadFile: '' as Resource<(file: File) => Promise<Ref<Blob>>>,
DeleteFile: '' as Resource<(id: string) => Promise<void>>
},
ids: {
PreviewWidget: '' as Ref<Widget>
},
string: {
Files: '' as IntlString,
NoFiles: '' as IntlString,
@ -106,6 +110,7 @@ export default plugin(attachmentId, {
FileBrowserTypeFilterPDFs: '' as IntlString,
DeleteFile: '' as IntlString,
Attachments: '' as IntlString,
FileBrowser: '' as IntlString
FileBrowser: '' as IntlString,
OpenInWindow: '' as IntlString
}
})

View File

@ -95,10 +95,12 @@
}
let objectChatPanel: ObjectChatPanel | undefined
let prevObjectId: Ref<Doc> | undefined = undefined
$: if (object._id) {
$: if (prevObjectId !== object._id) {
prevObjectId = object._id
objectChatPanel = hierarchy.classHierarchyMixin(object._class, chunter.mixin.ObjectChatPanel)
isAsideShown = objectChatPanel?.openByDefault === true
isAsideShown = isAsideShown ?? objectChatPanel?.openByDefault === true
}
</script>

View File

@ -259,7 +259,7 @@ export async function getWorkspaces (): Promise<Workspace[]> {
if (adays === bdays) {
return getWorkspaceSize(b) - getWorkspaceSize(a)
}
return bdays - adays
return b.lastVisit - a.lastVisit
})
return workspaces

View File

@ -354,6 +354,8 @@
.notification {
position: relative;
cursor: pointer;
user-select: none;
.embeddedMarker {
position: absolute;

View File

@ -196,4 +196,23 @@
<symbol id="undo" viewBox="0 0 32 32">
<path d="M20 10H7.8149L11.4023 6.4141L10 5L4 11L10 17L11.4023 15.5854L7.8179 12H20C21.5913 12 23.1174 12.6321 24.2426 13.7574C25.3679 14.8826 26 16.4087 26 18C26 19.5913 25.3679 21.1174 24.2426 22.2426C23.1174 23.3679 21.5913 24 20 24H13C12.4477 24 12 24.4477 12 25C12 25.5523 12.4477 26 13 26H20C22.1217 26 24.1566 25.1571 25.6569 23.6569C27.1571 22.1566 28 20.1217 28 18C28 15.8783 27.1571 13.8434 25.6569 12.3431C24.1566 10.8429 22.1217 10 20 10Z"/>
</symbol>
<symbol id="video" viewBox="0 0 32 32">
<path d="M13 15V23L20 19L13 15Z" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9 4C7.89543 4 7 4.89543 7 6V26C7 27.1046 7.89543 28 9 28H23C24.1046 28 25 27.1046 25 26V12H21C18.7909 12 17 10.2091 17 8V4H9ZM19 4.41421V8C19 9.10457 19.8954 10 21 10H24.5858L19 4.41421ZM5 6C5 3.79086 6.79086 2 9 2H18.5858C19.1162 2 19.6249 2.21071 20 2.58579L26.4142 9C26.7893 9.37507 27 9.88378 27 10.4142V26C27 28.2091 25.2091 30 23 30H9C6.79086 30 5 28.2091 5 26V6Z"
/>
</symbol>
<symbol id="audio" viewBox="0 0 32 32">
<path d="M13 15V23L20 19L13 15Z" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9 4C7.89543 4 7 4.89543 7 6V26C7 27.1046 7.89543 28 9 28H23C24.1046 28 25 27.1046 25 26V12H21C18.7909 12 17 10.2091 17 8V4H9ZM19 4.41421V8C19 9.10457 19.8954 10 21 10H24.5858L19 4.41421ZM5 6C5 3.79086 6.79086 2 9 2H18.5858C19.1162 2 19.6249 2.21071 20 2.58579L26.4142 9C26.7893 9.37507 27 9.88378 27 10.4142V26C27 28.2091 25.2091 30 23 30H9C6.79086 30 5 28.2091 5 26V6Z"
/>
</symbol>
<symbol id="file" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C7.89543 4 7 4.89543 7 6V26C7 27.1046 7.89543 28 9 28H23C24.1046 28 25 27.1046 25 26V12H21C18.7909 12 17 10.2091 17 8V4H9ZM19 4.41421V8C19 9.10457 19.8954 10 21 10H24.5858L19 4.41421ZM5 6C5 3.79086 6.79086 2 9 2H18.5858C19.1162 2 19.6249 2.21071 20 2.58579L26.4142 9C26.7893 9.37507 27 9.88378 27 10.4142V26C27 28.2091 25.2091 30 23 30H9C6.79086 30 5 28.2091 5 26V6ZM10 17C10 16.4477 10.4477 16 11 16H21C21.5523 16 22 16.4477 22 17C22 17.5523 21.5523 18 21 18H11C10.4477 18 10 17.5523 10 17ZM10 23C10 22.4477 10.4477 22 11 22H21C21.5523 22 22 22.4477 22 23C22 23.5523 21.5523 24 21 24H11C10.4477 24 10 23.5523 10 23Z" />
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -58,5 +58,8 @@ loadMetadata(view.icon, {
Copy: `${icons}#copy`,
DetailsFilled: `${icons}#details-filled`,
Translate: `${icons}#translate`,
Undo: `${icons}#undo`
Undo: `${icons}#undo`,
Video: `${icons}#video`,
Audio: `${icons}#audio`,
File: `${icons}#file`
})

View File

@ -254,7 +254,10 @@ const view = plugin(viewId, {
TodoList: '' as Asset,
DetailsFilled: '' as Asset,
Translate: '' as Asset,
Undo: '' as Asset
Undo: '' as Asset,
Video: '' as Asset,
Audio: '' as Asset,
File: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,

View File

@ -251,6 +251,7 @@
const doSyncLoc = reduceCalls(async (loc: Location): Promise<void> => {
if (workspaceId !== $location.path[1]) {
tabs = []
// Switch of workspace
return
}

View File

@ -64,7 +64,7 @@
}
async function updateTabData (tab: WorkbenchTab): Promise<void> {
const tabLoc = getTabLocation(tab)
const tabLoc = $tabIdStore === tab._id ? getCurrentLocation() : getTabLocation(tab)
const alias = tabLoc.path[2]
const application = client.getModel().findAllSync<Application>(workbench.class.Application, { alias })[0]

View File

@ -26,6 +26,7 @@ import ServerManager from './components/ServerManager.svelte'
import WorkbenchTabs from './components/WorkbenchTabs.svelte'
import { isAdminUser } from '@hcengineering/presentation'
import { canCloseTab, closeTab, pinTab, unpinTab } from './workbench'
import { closeWidgetTab, createWidgetTab } from './sidebar'
async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
return spaces.find((sp) => sp.archived) !== undefined
@ -54,7 +55,9 @@ export default async (): Promise<Resources> => ({
function: {
HasArchiveSpaces: hasArchiveSpaces,
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser(),
CanCloseTab: canCloseTab
CanCloseTab: canCloseTab,
CreateWidgetTab: createWidgetTab,
CloseWidgetTab: closeWidgetTab
},
actionImpl: {
Navigate: doNavigate,

View File

@ -23,7 +23,8 @@ import {
navigate,
getCurrentLocation
} from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import presentation, { getClient } from '@hcengineering/presentation'
import { getMetadata } from '@hcengineering/platform'
import { workspaceStore } from './utils'
@ -44,7 +45,12 @@ locationStore.subscribe((loc) => {
if (tab == null) return
const tabId = tab._id
if (tabId == null || tab._id !== tabId) return
const tabLoc = getTabLocation(tab)
const tabWs = tabLoc.path[1]
if (workspace !== tabWs) {
return
}
if (loc.path[2] === '' || loc.path[2] == null) return
void getClient().update(tab, { location: locationToUrl(loc) })
})
@ -86,7 +92,8 @@ export function selectTab (_id: Ref<WorkbenchTab>): void {
export function getTabLocation (tab: WorkbenchTab): Location {
const base = `${window.location.protocol}//${window.location.host}`
const url = new URL(concatLink(base, tab.location))
const front = getMetadata(presentation.metadata.FrontUrl) ?? base
const url = new URL(concatLink(front, tab.location))
return parseLocation(url)
}

View File

@ -257,6 +257,10 @@ export default plugin(workbenchId, {
WorkbenchExtensions: '' as ComponentExtensionId,
WorkbenchTabExtensions: '' as ComponentExtensionId
},
function: {
CreateWidgetTab: '' as Resource<(widget: Widget, tab: WidgetTab, newTab: boolean) => Promise<void>>,
CloseWidgetTab: '' as Resource<(widget: Widget, tab: string) => Promise<void>>
},
actionImpl: {
Navigate: '' as ViewAction<{
mode: 'app' | 'special' | 'space'

View File

@ -59,6 +59,10 @@ export abstract class PostgresDbCollection<T extends Record<string, any>> implem
}
protected buildWhereClause (query: Query<T>, lastRefIdx: number = 0): [string, any[]] {
if (Object.keys(query).length === 0) {
return ['', []]
}
const whereChunks: string[] = []
const values: any[] = []
let currIdx: number = lastRefIdx
@ -131,7 +135,9 @@ export abstract class PostgresDbCollection<T extends Record<string, any>> implem
const sqlChunks: string[] = [this.buildSelectClause()]
const [whereClause, whereValues] = this.buildWhereClause(query)
sqlChunks.push(whereClause)
if (whereClause !== '') {
sqlChunks.push(whereClause)
}
if (sort !== undefined) {
sqlChunks.push(this.buildSortClause(sort))
@ -200,7 +206,9 @@ export abstract class PostgresDbCollection<T extends Record<string, any>> implem
const [whereClause, whereValues] = this.buildWhereClause(query, updateValues.length)
sqlChunks.push(updateClause)
sqlChunks.push(whereClause)
if (whereClause !== '') {
sqlChunks.push(whereClause)
}
const finalSql = sqlChunks.join(' ')
await this.client.query(finalSql, [...updateValues, ...whereValues])
@ -210,7 +218,9 @@ export abstract class PostgresDbCollection<T extends Record<string, any>> implem
const sqlChunks: string[] = [`DELETE FROM ${this.name}`]
const [whereClause, whereValues] = this.buildWhereClause(query)
sqlChunks.push(whereClause)
if (whereClause !== '') {
sqlChunks.push(whereClause)
}
const finalSql = sqlChunks.join(' ')
await this.client.query(finalSql, whereValues)

View File

@ -8,7 +8,7 @@ export class LeadsPage {
}
readonly leadApplication = (): Locator => this.page.locator('[id="app-lead\\:string\\:LeadApplication"]')
readonly customersNavElement = (): Locator => this.page.locator('text=Customers')
readonly customersNavElement = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Customers')
readonly newCustomerButton = (): Locator => this.page.locator('button:has-text("New Customer")')
readonly personButton = (): Locator => this.page.locator('button:has-text("Person")')
readonly companyButton = (): Locator => this.page.locator('button:has-text("Company")')

View File

@ -11,7 +11,7 @@ export class CompanyDetailsPage extends CommonRecruitingPage {
}
readonly inputName = (): Locator => this.page.locator('div.antiEditBox input')
readonly buttonCompanyDetails = (): Locator => this.page.locator('div.flex-row-center > span', { hasText: 'Company' })
readonly buttonCompanyDetails = (): Locator => this.page.locator('.popupPanel-body__aside').locator('text=Company')
readonly buttonLocation = (): Locator =>
this.page.locator('//span[text()="Location"]/following-sibling::div[1]/button/span')

View File

@ -12,9 +12,11 @@ export class NavigationMenuPage {
readonly buttonMyApplications = (): Locator =>
this.page.locator('a[href$="my-applications"]', { hasText: 'My applications' })
readonly buttonTalents = (): Locator => this.page.locator('a[href$="talents"]', { hasText: 'Talents' })
readonly buttonVacancies = (): Locator => this.page.locator('a[href$="vacancies"]', { hasText: 'Vacancies' })
readonly buttonCompanies = (): Locator => this.page.locator('a[href$="organizations"]', { hasText: 'Companies' })
readonly navigator = (): Locator => this.page.locator('.antiPanel-navigator')
readonly buttonTalents = (): Locator => this.navigator().locator('a[href$="talents"]', { hasText: 'Talents' })
readonly buttonVacancies = (): Locator => this.navigator().locator('a[href$="vacancies"]', { hasText: 'Vacancies' })
readonly buttonCompanies = (): Locator =>
this.navigator().locator('a[href$="organizations"]', { hasText: 'Companies' })
// Action methods to click on each button
async clickButtonApplications (): Promise<void> {

View File

@ -8,8 +8,8 @@ export class RecruitingPage {
}
recruitApplication = (): Locator => this.page.locator('[id="app-recruit\\:string\\:RecruitApplication"]')
talentsNavElement = (): Locator => this.page.locator('text=Talents')
reviews = (): Locator => this.page.locator('text=Reviews')
talentsNavElement = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Talents')
reviews = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Reviews')
reviewButton = (): Locator => this.page.getByRole('button', { name: 'Review', exact: true })
frontendEngineerOption = (): Locator => this.page.locator('td:has-text("Frontend Engineer")')
@ -27,13 +27,13 @@ export class RecruitingPage {
newTalentModalPath = (): Locator => this.page.getByText('Person New Talent')
recruitApplicationButton = (): Locator => this.page.locator('[id="app-recruit\\:string\\:RecruitApplication"]')
applicationsLink = (): Locator => this.page.locator('text=/^Applications/')
talentsLink = (): Locator => this.page.locator('text=Talents')
vacanciesLink = (): Locator => this.page.locator('text=Vacancies')
talentsLink = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Talents')
vacanciesLink = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Vacancies')
softwareEngineerLink = (): Locator => this.page.locator('text=Software Engineer')
applicationLabelChunterButton = (): Locator =>
this.page.locator('[id="app-chunter\\:string\\:ApplicationLabelChunter"]')
generalChatLink = (): Locator => this.page.locator('text=general')
generalChatLink = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=general')
contactsButton = (): Locator => this.page.locator('[id="app-contact\\:string\\:Contacts"]')
employeeSection = (): Locator => this.page.getByRole('button', { name: 'Employee' })
johnAppleseed = (): Locator => this.page.locator('text=Appleseed John')

View File

@ -41,14 +41,14 @@ export class TalentsPage extends CommonRecruitingPage {
recruitApplicationButton = (): Locator => this.page.locator('[id="app-recruit\\:string\\:RecruitApplication"]')
talentsTab = (): Locator => this.page.locator('text=Talents')
talentsTab = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Talents')
newTalentButton = (): Locator => this.page.locator('button:has-text("New Talent")')
addSocialLinksButton = (): Locator => this.page.locator('[id="presentation\\:string\\:AddSocialLinks"]')
emailSelectorButton = (): Locator => this.page.locator('.antiPopup').locator('text=Email')
confirmEmailButton = (): Locator => this.page.locator('#channel-ok.antiButton')
createTalentButton = (): Locator => this.page.locator('.antiCard button:has-text("Create")')
popupPanel = (): Locator => this.page.locator('.popupPanel')
talentsLink = (): Locator => this.page.locator('text=Talents')
talentsLink = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Talents')
firstNameInput = (): Locator => this.page.locator('[placeholder="First name"]')
lastNameInput = (): Locator => this.page.locator('[placeholder="Last name"]')
skillsButton = (): Locator =>
@ -62,7 +62,7 @@ export class TalentsPage extends CommonRecruitingPage {
selectSkillButton = (skillName: string): Locator => this.page.locator(`button:has-text("${skillName}") .check`)
createCandidateButton = (): Locator => this.page.locator('button:has-text("Create")')
openOtherSkills = (): Locator => this.page.getByText('Other')
skillsLink = (): Locator => this.page.locator('text=Skills')
skillsLink = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Skills')
newSkillButton = (): Locator => this.page.getByRole('button', { name: 'Skill', exact: true })
emailContact = (): Locator =>
this.page.locator('div[class^="popupPanel-body__header"] button[id="gmail:string:Email"]')

View File

@ -44,7 +44,7 @@ export class VacanciesPage extends CommonRecruitingPage {
readonly recruitApplicationButton = (): Locator =>
this.page.locator('[id="app-recruit\\:string\\:RecruitApplication"]')
readonly vacanciesMenuLink = (): Locator => this.page.locator('text=Vacancies')
readonly vacanciesMenuLink = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Vacancies')
readonly createVacancyButton = (): Locator => this.page.locator('button:has-text("Vacancy")')
readonly vacancyInputField = (): Locator => this.page.locator('form [placeholder="Software\\ Engineer"]')
readonly createButton = (): Locator => this.page.locator('form button:has-text("Create")')

View File

@ -53,7 +53,7 @@ export class CommonTrackerPage extends CalendarPage {
submitDatePopup = (): Locator => this.page.locator('div.date-popup-container button[type="submit"]')
trackerApplicationButton = (): Locator => this.page.locator('[id="app-tracker\\:string\\:TrackerApplication"]')
componentsLink = (): Locator => this.page.locator('text=Components')
componentsLink = (): Locator => this.page.locator('.antiPanel-navigator').locator('text=Components')
createComponentButton = (): Locator => this.page.getByRole('button', { name: 'Component', exact: true })
componentNameInput = (): Locator => this.page.locator('[placeholder="Component\\ name"]')
createComponentConfirmButton = (): Locator => this.page.locator('button:has-text("Create component")')

View File

@ -8,7 +8,7 @@ import { NewIssue } from './types'
const retryOptions = { intervals: [1000, 1500, 2500], timeout: 60000 }
export class IssuesPage extends CommonTrackerPage {
modelSelectorAll = (): Locator => this.page.locator('label[data-id="tab-all"]')
issues = (): Locator => this.page.locator('text="Issues"')
issues = (): Locator => this.page.locator('.antiPanel-navigator').locator('text="Issues"')
subIssues = (): Locator => this.page.locator('button:has-text("Add sub-issue")')
newIssue = (): Locator => this.page.locator('#new-issue')
modelSelectorActive = (): Locator => this.page.locator('label[data-id="tab-active"]')
@ -109,7 +109,7 @@ export class IssuesPage extends CommonTrackerPage {
doneHeaderKanban = (): Locator => this.page.locator('.header :text-is("Done")').first()
canceledHeaderKanban = (): Locator => this.page.locator('.header :text-is("Canceled")').first()
myIssuesButton = (): Locator => this.page.locator('text="My issues"')
myIssuesButton = (): Locator => this.page.locator('.antiPanel-navigator').locator('text="My issues"')
assignedTab = (): Locator => this.page.locator('[data-id="tab-assigned"]')
createdTab = (): Locator => this.page.locator('[data-id="tab-created"]')
subscribedTab = (): Locator => this.page.locator('[data-id="tab-subscribed"]')
@ -140,7 +140,7 @@ export class IssuesPage extends CommonTrackerPage {
timeSpentReports = (): Locator => this.page.getByText('Time spent reports', { exact: true })
addTimeReport = (): Locator => this.page.locator('text="Add time report"')
issueName = (name: string): Locator => this.page.locator(`text="${name}"`)
issuesButton = (): Locator => this.page.locator('text="Issues"')
issuesButton = (): Locator => this.page.locator('.antiPanel-navigator').locator('text="Issues"')
viewButton = (): Locator => this.page.locator('button[data-id="btn-viewOptions"]')
orderingButton = (): Locator => this.page.locator('.ordering button')
modifiedDateMenuItem = (): Locator => this.page.locator('button.menu-item', { hasText: 'Modified date' })

View File

@ -344,7 +344,7 @@ test.describe('Planning ToDo tests', () => {
})
})
test('Change ToDo start and end times by dragging', async ({ page }) => {
test.skip('Change ToDo start and end times by dragging', async ({ page }) => {
const planningPage = new PlanningPage(page)
const planningNavigationMenuPage = new PlanningNavigationMenuPage(page)
const dateEnd = new Date()