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

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-01-17 17:22:23 +07:00
63 changed files with 1147 additions and 176 deletions
+25 -5
View File
@@ -1193,12 +1193,14 @@ export function devTool (
program
.command('copy-s3-datalake')
.description('migrate files from s3 to datalake')
.description('copy files from s3 to datalake')
.option('-w, --workspace <workspace>', 'Selected workspace only', '')
.option('-c, --concurrency <concurrency>', 'Number of files being processed concurrently', '10')
.action(async (cmd: { workspace: string, concurrency: string }) => {
.option('-e, --existing', 'Copy existing blobs', false)
.action(async (cmd: { workspace: string, concurrency: string, existing: boolean }) => {
const params = {
concurrency: parseInt(cmd.concurrency)
concurrency: parseInt(cmd.concurrency),
existing: cmd.existing
}
const storageConfig = storageConfigFromEnv(process.env.STORAGE)
@@ -1222,14 +1224,32 @@ export function devTool (
workspaces = workspaces
.filter((p) => isActiveMode(p.mode) || isArchivingMode(p.mode))
.filter((p) => cmd.workspace === '' || p.workspace === cmd.workspace)
.sort((a, b) => b.lastVisit - a.lastVisit)
// .sort((a, b) => b.lastVisit - a.lastVisit)
.sort((a, b) => {
if (a.backupInfo !== undefined && b.backupInfo !== undefined) {
return b.backupInfo.blobsSize - a.backupInfo.blobsSize
} else if (b.backupInfo !== undefined) {
return 1
} else if (a.backupInfo !== undefined) {
return -1
} else {
return b.lastVisit - a.lastVisit
}
})
})
const count = workspaces.length
console.log('found workspaces', count)
let index = 0
for (const workspace of workspaces) {
index++
toolCtx.info('processing workspace', { workspace: workspace.workspace, index, count })
toolCtx.info('processing workspace', {
workspace: workspace.workspace,
index,
count,
blobsSize: workspace.backupInfo?.blobsSize ?? 0
})
const workspaceId = getWorkspaceId(workspace.workspace)
for (const config of storages) {
+36 -6
View File
@@ -261,6 +261,7 @@ async function retryOnFailure<T> (
export interface CopyDatalakeParams {
concurrency: number
existing: boolean
}
export async function copyToDatalake (
@@ -281,7 +282,9 @@ export async function copyToDatalake (
let time = Date.now()
let processedCnt = 0
let processedSize = 0
let skippedCnt = 0
let existingCnt = 0
let failedCnt = 0
function printStats (): void {
@@ -291,14 +294,32 @@ export async function copyToDatalake (
processedCnt,
'skipped',
skippedCnt,
'existing',
existingCnt,
'failed',
failedCnt,
Math.round(duration / 1000) + 's'
Math.round(duration / 1000) + 's',
formatSize(processedSize)
)
time = Date.now()
}
const existing = new Set<string>()
let cursor: string | undefined = ''
let hasMore = true
while (hasMore) {
const res = await datalake.listObjects(ctx, workspaceId, cursor, 1000)
cursor = res.cursor
hasMore = res.cursor !== undefined
for (const blob of res.blobs) {
existing.add(blob.name)
}
}
console.info('found blobs in datalake:', existing.size)
const rateLimiter = new RateLimiter(params.concurrency)
const iterator = await adapter.listStream(ctx, workspaceId)
@@ -315,6 +336,12 @@ export async function copyToDatalake (
continue
}
if (!params.existing && existing.has(objectName)) {
// TODO handle mutable blobs
existingCnt++
continue
}
await rateLimiter.add(async () => {
try {
await retryOnFailure(
@@ -323,6 +350,7 @@ export async function copyToDatalake (
async () => {
await copyBlobToDatalake(ctx, workspaceId, blob, config, adapter, datalake)
processedCnt += 1
processedSize += blob.size
},
50
)
@@ -352,11 +380,6 @@ export async function copyBlobToDatalake (
datalake: DatalakeClient
): Promise<void> {
const objectName = blob._id
const stat = await datalake.statObject(ctx, workspaceId, objectName)
if (stat !== undefined) {
return
}
if (blob.size < 1024 * 1024 * 64) {
// Handle small file
const { endpoint, accessKey: accessKeyId, secretKey: secretAccessKey, region } = config
@@ -392,3 +415,10 @@ export async function copyBlobToDatalake (
}
}
}
export function formatSize (size: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const pow = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024))
const val = (1.0 * size) / Math.pow(1024, pow)
return `${val.toFixed(2)} ${units[pow]}`
}
+11
View File
@@ -696,6 +696,17 @@ export function createModel (builder: Builder): void {
contact.channelProvider.Profile
)
builder.createDoc(
contact.class.ChannelProvider,
core.space.Model,
{
label: contact.string.Viber,
icon: contact.icon.Viber,
placeholder: contact.string.ViberPlaceholder
},
contact.channelProvider.Viber
)
builder.createDoc(
contact.class.AvatarProvider,
core.space.Model,
+2
View File
@@ -91,6 +91,8 @@ export default mergeIds(contactId, contact, {
SkypePlaceholder: '' as IntlString,
Profile: '' as IntlString,
ProfilePlaceholder: '' as IntlString,
Viber: '' as IntlString,
ViberPlaceholder: '' as IntlString,
CurrentEmployee: '' as IntlString,
+18
View File
@@ -676,6 +676,24 @@ export function createModel (builder: Builder): void {
provider: documents.function.DocumentIdentifierProvider
})
createAction(
builder,
{
action: documents.actionImpl.TransferDocument,
label: documents.string.Transfer,
icon: view.icon.Move,
input: 'any',
category: view.category.General,
target: documents.class.ProjectDocument,
visibilityTester: documents.function.CanTransferDocument,
context: {
mode: ['context', 'browser'],
group: 'copy'
}
},
documents.action.TransferDocument
)
createAction(
builder,
{
@@ -61,8 +61,10 @@ export default mergeIds(documentsId, documents, {
CreateChildTemplate: '' as ViewAction,
CreateDocument: '' as ViewAction,
CreateTemplate: '' as ViewAction,
TransferTemplate: '' as ViewAction,
DeleteDocument: '' as ViewAction,
ArchiveDocument: '' as ViewAction,
TransferDocument: '' as ViewAction,
EditDocSpace: '' as ViewAction
},
viewlet: {
+20 -1
View File
@@ -549,6 +549,23 @@ export async function getBlobURL (blob: Blob): Promise<string> {
})
}
/**
* @public
*/
export function copyTextToClipboardOldBrowser (text: string): void {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.classList.add('hulyClipboardArea')
document.body.appendChild(textarea)
textarea.select()
try {
document.execCommand('copy')
} catch (err) {
console.error(err)
}
document.body.removeChild(textarea)
}
/**
* @public
*/
@@ -562,7 +579,9 @@ export async function copyTextToClipboard (text: string | Promise<string>): Prom
await navigator.clipboard.write([clipboardItem])
} catch {
// Fallback to default clipboard API implementation
await navigator.clipboard.writeText(text instanceof Promise ? await text : text)
if (navigator.clipboard != null && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(text instanceof Promise ? await text : text)
} else copyTextToClipboardOldBrowser(text instanceof Promise ? await text : text)
}
}
+1
View File
@@ -24,6 +24,7 @@ export * from './nodes'
export * from './marks/code'
export * from './marks/colors'
export * from './marks/noteBase'
export * from './marks/inlineComment'
export * from './markdown'
export * from './markdown/serializer'
export * from './markdown/parser'
+2
View File
@@ -37,6 +37,7 @@ import { MermaidExtension, mermaidOptions } from '../nodes/mermaid'
import TextAlign from '@tiptap/extension-text-align'
import TextStyle from '@tiptap/extension-text-style'
import { BackgroundColor, TextColor } from '../marks/colors'
import { InlineCommentMark } from '../marks/inlineComment'
const headingLevels: Level[] = [1, 2, 3, 4, 5, 6]
@@ -84,6 +85,7 @@ export const ServerKit = Extension.create<ServerKitOptions>({
levels: headingLevels
}
}),
InlineCommentMark.configure({}),
CodeBlockExtension.configure(codeBlockOptions),
CodeExtension.configure(codeOptions),
MermaidExtension.configure(mermaidOptions),
+87
View File
@@ -0,0 +1,87 @@
//
// Copyright © 2025 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 { Mark } from '@tiptap/core'
import { Fragment, Node, Slice } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
export const InlineCommentMark = Mark.create({
name: 'inline-comment',
excludes: '',
inclusive: false,
parseHTML () {
return [
{
tag: 'span.proseInlineComment[data-inline-comment-thread]'
}
]
},
renderHTML ({ HTMLAttributes, mark }) {
return ['span', { ...HTMLAttributes, class: 'proseInlineComment' }, 0]
},
addAttributes () {
const name = 'data-inline-comment-thread-id'
return {
thread: {
default: undefined,
parseHTML: (element) => {
return element.getAttribute(name)
},
renderHTML: (attributes) => {
return { [name]: attributes.thread }
}
}
}
},
addProseMirrorPlugins () {
return [...(this.parent?.() ?? []), InlineCommentPasteFixPlugin()]
}
})
function removeMarkFromNode (node: Node, name: string): Node {
if (node.isText) {
return node.mark(node.marks.filter((mark) => mark.type.name !== name))
}
if (node.content.size > 0) {
const nodes: Node[] = []
node.content.forEach((child) => {
nodes.push(removeMarkFromNode(child, name))
})
return node.copy(Fragment.fromArray(nodes))
}
return node
}
export function InlineCommentPasteFixPlugin (): Plugin {
return new Plugin({
key: new PluginKey('inline-comment-paste-fix-plugin'),
props: {
transformPasted: (slice) => {
const nodes: Node[] = []
slice.content.forEach((node) => {
nodes.push(removeMarkFromNode(node, 'inline-comment'))
})
return new Slice(Fragment.fromArray(nodes), slice.openStart, slice.openEnd)
}
}
})
}
+2
View File
@@ -37,6 +37,8 @@ export const backtickInputRegex = /^```$/
export const tildeInputRegex = /^~~~$/
export const CodeBlockExtension = CodeBlock.extend({
marks: 'inline-comment',
addAttributes () {
return {
language: {
+1
View File
@@ -24,6 +24,7 @@ export const mermaidOptions: CodeBlockOptions = {
export const MermaidExtension = CodeBlock.extend({
name: 'mermaid',
group: 'block',
marks: 'inline-comment',
parseHTML () {
return [
+2
View File
@@ -250,6 +250,7 @@
--theme-text-editor-note-anchor-bg-primary-light: #747C81;
--text-editor-table-border-color: hsl(220, 6%, 40%);
--text-editor-color-picker-outline: rgba(250, 222, 201, 0.3);
--theme-text-editor-palette-text-gray: rgba(155, 155, 155, 1);
--theme-text-editor-palette-text-brown: rgba(186, 133, 111, 1);
@@ -531,6 +532,7 @@
--theme-text-editor-note-anchor-bg-primary-light: #D5E5F5;
--text-editor-table-border-color: #c9cbcd;
--text-editor-color-picker-outline: rgb(227, 226, 224);
--theme-text-editor-palette-text-gray: rgba(120, 119, 116, 1);
--theme-text-editor-palette-text-brown: rgba(159, 107, 83, 1);
+5
View File
@@ -917,6 +917,11 @@ a.no-line {
.text-line-through { text-decoration: line-through; }
.hulyClipboardArea {
width: 0;
height: 0;
opacity: 0;
}
.hidden-text {
position: absolute;
visibility: hidden;
@@ -16,6 +16,7 @@
import { Timestamp } from '@hcengineering/core'
import DueDatePopup from './DueDatePopup.svelte'
import { tooltip } from '../../tooltips'
import ui from '../../plugin'
import DatePresenter from './DatePresenter.svelte'
import { getDaysDifference, getDueDateIconModifier, getFormattedDate } from './internal/DateUtils'
import { ButtonKind, ButtonSize } from '../../types'
@@ -67,6 +68,7 @@
: undefined}
>
<DatePresenter
labelNull={ui.string.DueDate}
{value}
{editable}
{iconModifier}
-1
View File
@@ -320,7 +320,6 @@ export const deviceOptionsStore = writable<DeviceOptions>({
isPortrait: false,
isMobile: false,
navigator: { visible: true, float: false, direction: 'vertical' },
aside: { visible: true, float: false },
fontSize: 0,
size: null,
sizes: { xs: false, sm: false, md: false, lg: false, xl: false, xxl: false },
+3 -3
View File
@@ -375,9 +375,9 @@ export function fitPopupElement (
} else if (element === 'full-centered') {
const rect = contentPanel !== undefined ? contentPanel.getBoundingClientRect() : { top: 0 }
newProps.top = `${Math.max(20, rect.top + 1)}px`
newProps.bottom = '20px'
newProps.left = '20px'
newProps.right = '20px'
newProps.bottom = '.5rem'
newProps.left = '.5rem'
newProps.right = '.5rem'
show = true
} else if (element === 'content' && contentPanel !== undefined) {
const rect = contentPanel.getBoundingClientRect()
-1
View File
@@ -381,7 +381,6 @@ export interface DeviceOptions {
isPortrait: boolean
isMobile: boolean
navigator: { visible: boolean, float: boolean, direction: 'vertical' | 'horizontal' }
aside: { visible: boolean, float: boolean }
fontSize: number
size: WidthType | null
sizes: Record<WidthType, boolean>
+1 -6
View File
@@ -5,8 +5,7 @@ import {
getLocation,
type Location,
navigate,
languageStore,
deviceOptionsStore as deviceInfo
languageStore
} from '@hcengineering/ui'
import { type Ref, type Doc, type Class, generateId } from '@hcengineering/core'
import activity, { type ActivityMessage } from '@hcengineering/activity'
@@ -180,10 +179,6 @@ export async function replyToThread (message: ActivityMessage, e: Event): Promis
const fromSidebar = isElementFromSidebar(e.target as HTMLElement)
const loc = getCurrentLocation()
const dev = get(deviceInfo)
dev.aside.visible = true
deviceInfo.set(dev)
threadMessagesStore.set(message)
if (fromSidebar) {
+26
View File
@@ -95,4 +95,30 @@
<path d="M9.6,16.2c-2.1,0-3.9-1.9-4-4.3c-0.1-1.2,0.3-2.3,1-3.1c0.8-0.8,1.8-1.2,3-1.2c1.2,0,2.2,0.4,3,1.3c0.8,0.8,1.1,1.9,1.1,3.1C13.5,14.3,11.7,16.2,9.6,16.2z M9.6,9.5C9,9.5,8.4,9.7,8,10.1c-0.4,0.4-0.6,1-0.5,1.7c0.1,1.4,1.1,2.5,2.2,2.5s2.1-1.2,2.2-2.5c0-0.7-0.2-1.3-0.6-1.7C10.9,9.7,10.3,9.5,9.6,9.5z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M2,22.5c0.9-3.5,4.3-5.2,7.6-5.2c1.3,0,2.6,0.2,3.8,0.8c0.5,0.2,0.7,0.8,0.5,1.2c-0.2,0.5-0.8,0.7-1.2,0.5c-0.9-0.4-1.9-0.6-3.1-0.6c-2.6,0-4.9,1.2-5.7,3.4H10c0.5,0,0.9,0.4,0.9,0.9s-0.4,0.9-0.9,0.9H3.5C2.4,24.4,1.8,23.3,2,22.5L2,22.5z" />
</symbol>
<symbol id="viber" viewBox="0 0 52.511 52.511">
<path d="M31.256,0H21.254C10.778,0,2.255,8.521,2.255,18.995v9.01c0,7.8,4.793,14.81,12,17.665v5.841
c0,0.396,0.233,0.754,0.595,0.914c0.13,0.058,0.268,0.086,0.405,0.086c0.243,0,0.484-0.089,0.671-0.259L21.725,47h9.531
c10.476,0,18.999-8.521,18.999-18.995v-9.01C50.255,8.521,41.732,0,31.256,0z M48.255,28.005C48.255,37.376,40.63,45,31.256,45
h-9.917c-0.248,0-0.487,0.092-0.671,0.259l-4.413,3.997v-4.279c0-0.424-0.267-0.802-0.667-0.942
C8.81,41.638,4.255,35.196,4.255,28.005v-9.01C4.255,9.624,11.881,2,21.254,2h10.002c9.374,0,16.999,7.624,16.999,16.995V28.005z"
/>
<path d="M39.471,30.493l-6.146-3.992c-0.672-0.437-1.472-0.585-2.255-0.423c-0.784,0.165-1.458,0.628-1.895,1.303l-0.289,0.444
c-2.66-0.879-5.593-2.002-7.349-7.085l0.727-0.632h0c1.248-1.085,1.379-2.983,0.294-4.233l-4.808-5.531
c-0.362-0.417-0.994-0.46-1.411-0.099l-3.019,2.624c-2.648,2.302-1.411,5.707-1.004,6.826c0.018,0.05,0.04,0.098,0.066,0.145
c0.105,0.188,2.612,4.662,6.661,8.786c4.065,4.141,11.404,7.965,11.629,8.076c0.838,0.544,1.781,0.805,2.714,0.805
c1.638,0,3.244-0.803,4.202-2.275l2.178-3.354C40.066,31.413,39.934,30.794,39.471,30.493z M35.91,34.142
c-0.901,1.388-2.763,1.782-4.233,0.834c-0.073-0.038-7.364-3.835-11.207-7.75c-3.592-3.659-5.977-7.724-6.302-8.291
c-0.792-2.221-0.652-3.586,0.464-4.556l2.265-1.968l4.152,4.776c0.369,0.424,0.326,1.044-0.096,1.411l-1.227,1.066
c-0.299,0.26-0.417,0.671-0.3,1.049c2.092,6.798,6.16,8.133,9.13,9.108l0.433,0.143c0.433,0.146,0.907-0.021,1.155-0.403
l0.709-1.092c0.146-0.226,0.37-0.379,0.63-0.434c0.261-0.056,0.527-0.004,0.753,0.143l5.308,3.447L35.91,34.142z"/>
<path d="M28.538,16.247c-0.532-0.153-1.085,0.156-1.236,0.688c-0.151,0.531,0.157,1.084,0.688,1.235
c1.49,0.424,2.677,1.613,3.097,3.104c0.124,0.44,0.525,0.729,0.962,0.729c0.09,0,0.181-0.012,0.272-0.037
c0.531-0.15,0.841-0.702,0.691-1.234C32.405,18.578,30.69,16.859,28.538,16.247z"/>
<path d="M36.148,22.219c0.09,0,0.181-0.012,0.272-0.037c0.532-0.15,0.841-0.703,0.691-1.234c-1.18-4.183-4.509-7.519-8.689-8.709
c-0.531-0.153-1.084,0.158-1.235,0.689c-0.151,0.531,0.157,1.084,0.688,1.235c3.517,1,6.318,3.809,7.311,7.328
C35.311,21.931,35.711,22.219,36.148,22.219z"/>
<path d="M27.991,7.582c-0.532-0.153-1.085,0.156-1.236,0.689c-0.151,0.531,0.157,1.084,0.688,1.235
c5.959,1.695,10.706,6.453,12.388,12.416c0.124,0.44,0.525,0.729,0.962,0.729c0.09,0,0.181-0.012,0.272-0.037
c0.531-0.15,0.841-0.703,0.691-1.234C39.887,14.753,34.613,9.467,27.991,7.582z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+3 -1
View File
@@ -105,6 +105,8 @@
"For": "Pro",
"SelectUsers": "Vyberte uživatele",
"AddGuest": "Přidat hosta",
"ViewProfile": "Zobrazit profil"
"ViewProfile": "Zobrazit profil",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+3 -1
View File
@@ -105,6 +105,8 @@
"For": "Für",
"SelectUsers": "Benutzer auswählen",
"AddGuest": "Gast hinzufügen",
"ViewProfile": "Profil anzeigen"
"ViewProfile": "Profil anzeigen",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+3 -1
View File
@@ -105,6 +105,8 @@
"For": "For",
"SelectUsers": "Select users",
"AddGuest": "Add guest",
"ViewProfile": "View profile"
"ViewProfile": "View profile",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+3 -1
View File
@@ -105,6 +105,8 @@
"For": "Para",
"SelectUsers": "Seleccionar usuarios",
"AddGuest": "Añadir invitado",
"ViewProfile": "Ver perfil"
"ViewProfile": "Ver perfil",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+3 -1
View File
@@ -105,6 +105,8 @@
"For": "Pour",
"SelectUsers": "Sélectionner des utilisateurs",
"AddGuest": "Ajouter un invité",
"ViewProfile": "Voir le profil"
"ViewProfile": "Voir le profil",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+3 -1
View File
@@ -105,6 +105,8 @@
"For": "Per",
"SelectUsers": "Seleziona utenti",
"AddGuest": "Aggiungi ospite",
"ViewProfile": "Visualizza profilo"
"ViewProfile": "Visualizza profilo",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+3 -1
View File
@@ -105,6 +105,8 @@
"For": "Para",
"SelectUsers": "Selecionar utilizadores",
"AddGuest": "Adicionar convidado",
"ViewProfile": "Ver perfil"
"ViewProfile": "Ver perfil",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+3 -1
View File
@@ -105,6 +105,8 @@
"For": "Для",
"SelectUsers": "Выберите пользователей",
"AddGuest": "Добавить гостя",
"ViewProfile": "Посмотреть профиль"
"ViewProfile": "Посмотреть профиль",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+3 -1
View File
@@ -105,6 +105,8 @@
"For": "为",
"SelectUsers": "选择用户",
"AddGuest": "添加访客",
"ViewProfile": "查看资料"
"ViewProfile": "查看资料",
"Viber": "Viber",
"ViberPlaceholder": "Viber"
}
}
+1
View File
@@ -28,6 +28,7 @@ loadMetadata(contact.icon, {
Telegram: `${icons}#telegram`,
Twitter: `${icons}#twitter`,
VK: `${icons}#vk`,
Viber: `${icons}#viber`,
WhatsApp: `${icons}#whatsapp`,
Skype: `${icons}#skype`,
Youtube: `${icons}#youtube`,
+4 -2
View File
@@ -235,7 +235,8 @@ export const contactPlugin = plugin(contactId, {
Homepage: '' as Ref<ChannelProvider>,
Whatsapp: '' as Ref<ChannelProvider>,
Skype: '' as Ref<ChannelProvider>,
Profile: '' as Ref<ChannelProvider>
Profile: '' as Ref<ChannelProvider>,
Viber: '' as Ref<ChannelProvider>
},
avatarProvider: {
Color: '' as Ref<AvatarProvider>,
@@ -273,7 +274,8 @@ export const contactPlugin = plugin(contactId, {
ComponentMembers: '' as Asset,
Profile: '' as Asset,
KickUser: '' as Asset,
Contacts: '' as Asset
Contacts: '' as Asset,
Viber: '' as Asset
},
space: {
Contacts: '' as Ref<Space>
@@ -129,7 +129,12 @@
"Copy": "kopírovat",
"ConfigLabel": "Řízené dokumenty",
"ConfigDescription": "Rozšíření pro správu řízených dokumentů"
"ConfigDescription": "Rozšíření pro správu řízených dokumentů",
"Transfer": "Přenos",
"TransferWarning": "Někteří členové týmu mohou po této akci ztratit možnost prohlížet nebo upravovat tento dokument.",
"TransferDocuments": "Přenos řízených dokumentů",
"TransferDocumentsHint": "Dokumenty, které mají být přeneseny do vybraného prostoru:"
},
"controlledDocStates": {
"Empty": "",
@@ -293,7 +293,12 @@
"DeleteDocumentCategoryPermission": "Dokumentenkategorie löschen",
"DeleteDocumentCategoryDescription": "Gewährt Benutzern die Möglichkeit, eine Dokumentenkategorie zu löschen",
"ConfigLabel": "Kontrollierte Dokumente",
"ConfigDescription": "Erweiterung zur Verwaltung kontrollierter Dokumente"
"ConfigDescription": "Erweiterung zur Verwaltung kontrollierter Dokumente",
"Transfer": "Übertragung",
"TransferWarning": "Einige Teammitglieder können dieses Dokument nach dieser Aktion möglicherweise nicht mehr anzeigen oder bearbeiten.",
"TransferDocuments": "Übertragung kontrollierter Dokumente",
"TransferDocumentsHint": "Dokumente, die in den ausgewählten Bereich übertragen werden sollen:"
},
"controlledDocStates": {
"Empty": "",
@@ -295,7 +295,12 @@
"DeleteDocumentCategoryPermission": "Delete document category",
"DeleteDocumentCategoryDescription": "Grants users ability to delete a document category",
"ConfigLabel": "Controlled Documents",
"ConfigDescription": "Extension to manage controlled documents"
"ConfigDescription": "Extension to manage controlled documents",
"Transfer": "Transfer",
"TransferWarning": "Some team members may lose the ability to view or edit this document after this action.",
"TransferDocuments": "Transfer controlled documents",
"TransferDocumentsHint": "Documents to be transferred to the selected space:"
},
"controlledDocStates": {
"Empty": "",
@@ -253,7 +253,12 @@
"DeleteDocumentCategoryPermission": "Supprimer la catégorie de document",
"DeleteDocumentCategoryDescription": "Accorde aux utilisateurs la capacité de supprimer une catégorie de document",
"ConfigLabel": "Documents contrôlés",
"ConfigDescription": "Extension pour gérer les documents contrôlés"
"ConfigDescription": "Extension pour gérer les documents contrôlés",
"Transfer": "Transfert",
"TransferWarning": "Certains membres de l'équipe peuvent perdre la possibilité de visualiser ou de modifier ce document après cette action.",
"TransferDocuments": "Transférer des documents contrôlés",
"TransferDocumentsHint": "Documents à transférer dans l'espace sélectionné:"
},
"controlledDocStates": {
"Empty": "",
@@ -251,7 +251,12 @@
"DeleteDocumentCategoryPermission": "Elimina categoria documento",
"DeleteDocumentCategoryDescription": "Concede agli utenti la possibilità di eliminare una categoria di documento",
"ConfigLabel": "Documenti controllati",
"ConfigDescription": "Estensione per gestire documenti controllati"
"ConfigDescription": "Estensione per gestire documenti controllati",
"Transfer": "Trasferimento",
"TransferWarning": "Alcuni membri del team potrebbero perdere la possibilità di visualizzare o modificare il documento dopo questa azione.",
"TransferDocuments": "Trasferimento di documenti controllati",
"TransferDocumentsHint": "Documenti da trasferire nello spazio selezionato:"
},
"controlledDocStates": {
"Empty": "",
@@ -295,7 +295,12 @@
"DeleteDocumentCategoryPermission": "Удалять категорию",
"DeleteDocumentCategoryDescription": "Предоставляет пользователям разрешение удалять категорию",
"ConfigLabel": "Управляемые Документы",
"ConfigDescription": "Расширение для управления управляемыми документами"
"ConfigDescription": "Расширение для управления управляемыми документами",
"Transfer": "Трансфер",
"TransferWarning": "После этого действия некоторые члены команды могут потерять возможность просматривать или редактировать этот документ.",
"TransferDocuments": "Трансфер управляемых документов",
"TransferDocumentsHint": "Документы, которые будут перенесены в выбранное пространство:"
},
"controlledDocStates": {
"Empty": "",
@@ -292,7 +292,12 @@
"DeleteDocumentCategoryPermission": "删除文档类别",
"DeleteDocumentCategoryDescription": "授予用户删除文档类别的权限",
"ConfigLabel": "受控文档",
"ConfigDescription": "用于管理受控文档的扩展"
"ConfigDescription": "用于管理受控文档的扩展",
"Transfer": "转让",
"TransferWarning": "执行此操作后,某些团队成员可能会失去查看或编辑此文档的能力",
"TransferDocuments": "移交受控文件",
"TransferDocumentsHint": "要转移到所选空间的文件:"
},
"controlledDocStates": {
"Empty": "",
@@ -0,0 +1,284 @@
<!--
// Copyright © 2025 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 documents, {
canTransferDocuments,
DocumentMeta,
DocumentTransferRequest,
listDocumentsAffectedByTransfer,
transferDocuments,
type DocumentSpace,
type DocumentSpaceType,
type Project,
type ProjectDocument
} from '@hcengineering/controlled-documents'
import { type Doc, type Ref, type Space } from '@hcengineering/core'
import presentation, { getClient, SpaceSelector } from '@hcengineering/presentation'
import { Button, Label } from '@hcengineering/ui'
import { permissionsStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import documentsRes from '../../../plugin'
import { getLatestProjectId } from '../../../utils'
import DocumentParentSelector from '../../hierarchy/DocumentParentSelector.svelte'
import ProjectSelector from '../../project/ProjectSelector.svelte'
import Info from '../../icons/Info.svelte'
export let sourceDocumentIds: Ref<DocumentMeta>[] = []
export let sourceSpaceId: Ref<DocumentSpace> | undefined
export let sourceProjectId: Ref<Project<DocumentSpace>> | undefined
let targetSpaceId: Ref<DocumentSpace> | undefined
let targetParentId: Ref<DocumentMeta> | undefined
let targetSpace: DocumentSpace | undefined
$: void fetchSpace(targetSpaceId)
let targetSpaceType: DocumentSpaceType | undefined
$: void fetchSpaceType(targetSpace?.type)
let targetProjectId: Ref<Project> | undefined
$: void selectProject(targetSpaceId)
let targetParentDocumentId: Ref<ProjectDocument> | undefined
let affectedDocs: DocumentMeta[] = []
let canTransfer = false
$: request =
sourceSpaceId !== undefined && targetSpaceId !== undefined
? ({
sourceDocumentIds,
sourceSpaceId,
sourceProjectId,
targetSpaceId,
targetProjectId,
targetParentId
} satisfies DocumentTransferRequest)
: undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
async function transfer (): Promise<void> {
if (request !== undefined) {
await transferDocuments(client, request)
dispatch('close')
}
}
$: if (request !== undefined) {
void listDocumentsAffectedByTransfer(client, request).then((result) => {
affectedDocs = result
})
}
$: if (request !== undefined) {
void canTransferDocuments(client, request).then((value) => {
canTransfer = value
})
} else {
canTransfer = false
}
async function selectProject (spaceRef: Ref<DocumentSpace> | undefined): Promise<void> {
targetProjectId = spaceRef !== undefined ? await getLatestProjectId(spaceRef) : undefined
}
async function fetchSpace (id: Ref<DocumentSpace> | undefined): Promise<void> {
targetSpace = id === undefined ? undefined : await client.findOne(documents.class.DocumentSpace, { _id: id })
}
async function fetchSpaceType (id: Ref<DocumentSpaceType> | undefined): Promise<void> {
targetSpaceType =
id === undefined ? undefined : await client.findOne(documents.class.DocumentSpaceType, { _id: id }, {})
}
async function handleParentSelected (doc: Doc): Promise<void> {
if (hierarchy.isDerived(doc._class, documents.class.DocumentSpace)) {
targetParentDocumentId = undefined
targetParentId = undefined
} else if (hierarchy.isDerived(doc._class, documents.class.ProjectDocument)) {
const pjDoc = doc as ProjectDocument
targetParentDocumentId = pjDoc._id
const pjMeta = await client.findOne(documents.class.ProjectMeta, { _id: pjDoc.attachedTo })
if (targetParentDocumentId === pjDoc._id) targetParentId = pjMeta?.meta
}
}
function handleProjectSelected (value: Ref<Project> | undefined): void {
targetProjectId = value
}
let haveTemplateObjects: boolean = false
$: void checkForTemplateObjects(affectedDocs)
async function checkForTemplateObjects (docs: DocumentMeta[]): Promise<void> {
const cdocs = await client.findAll(documents.class.ControlledDocument, {
attachedTo: { $in: docs.map((d) => d._id) }
})
haveTemplateObjects = cdocs.some((doc) => hierarchy.hasMixin(doc, documents.mixin.DocumentTemplate))
}
const externalSpaces = hierarchy.getDescendants(documents.class.ExternalSpace)
$: hasParentSelector = targetSpaceId !== documents.space.UnsortedTemplates
$: permissionRestrictedSpaces = Object.entries($permissionsStore.ps)
.filter(([, pss]) => !pss.has(documents.permission.CreateDocument))
.map(([s]) => s) as Ref<Space>[]
$: restrictedSpaces =
sourceSpaceId !== undefined ? permissionRestrictedSpaces.concat(sourceSpaceId) : permissionRestrictedSpaces
$: spaceQuery = haveTemplateObjects
? { _id: { $nin: restrictedSpaces }, archived: false, _class: { $nin: externalSpaces } }
: { _id: { $nin: restrictedSpaces }, archived: false }
</script>
<div class="popup">
<div class="bottom-divider">
<div class="text-xl pr-6 pl-6 pt-4 pb-4 primary-text-color">
<Label label={documents.string.TransferDocuments} />
</div>
</div>
<div class="p-6 bottom-divider popup-body">
<div class="sectionTitle"><Label label={documentsRes.string.Space} /></div>
<div class="flex-row-center flex-no-shrink flex-gap-4">
<div class="space">
<SpaceSelector
_class={documents.class.DocumentSpace}
query={spaceQuery}
bind:space={targetSpaceId}
label={documentsRes.string.Space}
width="100%"
justify="left"
autoSelect={true}
/>
</div>
{#if targetSpace && targetSpaceType && targetSpaceType.projects}
<div class="space">
<ProjectSelector
value={targetProjectId}
space={targetSpace._id}
kind={'no-border'}
size={'small'}
justify="left"
showReadonly={false}
on:change={(e) => {
handleProjectSelected(e.detail)
}}
/>
</div>
{/if}
</div>
<div class="parentText pt-4"><Label label={documents.string.TransferDocumentsHint} /></div>
<ol class="docList">
{#each affectedDocs as object}
<li>{object.title}</li>
{/each}
</ol>
{#if hasParentSelector}
<div class="sectionTitle"><Label label={documents.string.Parent} /></div>
<div class="parentText"><Label label={documentsRes.string.SelectParent} /></div>
<div class="parentSelector">
{#if targetSpace}
<DocumentParentSelector
space={targetSpace}
project={targetProjectId}
selected={targetParentDocumentId}
collapsedPrefix="locationStep"
on:selected={(e) => {
void handleParentSelected(e.detail)
}}
/>
{/if}
</div>
{/if}
</div>
<div class="flex items-center flex-between pr-6 pl-6 pt-4 pb-4">
<div class="flex flex-gap-2 items-center max-w-120 p-1 text-xs pr-4">
<div class="warning-sign">
<Info size="small" />
</div>
<Label label={documents.string.TransferWarning} />
</div>
<div class="flex justify-end items-center flex-gap-2">
<Button kind="regular" label={presentation.string.Cancel} on:click={() => dispatch('close')} />
<Button
kind={canTransfer ? 'primary' : 'ghost'}
disabled={!canTransfer}
label={documents.string.Transfer}
on:click={transfer}
/>
</div>
</div>
</div>
<style lang="scss">
.popup {
width: 58.25rem;
border-radius: 1.25rem;
background-color: var(--theme-dialog-background-color);
}
.docList li {
color: var(--global-primary-TextColor);
}
.popup-body {
height: 60vh;
overflow-y: auto;
}
.hint {
color: var(--theme-dark-color);
}
.warning-sign {
color: var(--theme-docs-warning-icon-color);
}
.primary-text-color {
color: var(--theme-text-primary-color);
}
.sectionTitle {
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
&:not(:first-child) {
margin-top: 1.5rem;
}
}
.space {
width: 12.5rem;
margin-top: 0.5rem;
}
.parentText {
font-size: 0.6875rem;
line-height: 1rem;
}
.parentSelector {
margin-top: 0.5rem;
}
</style>
@@ -28,7 +28,9 @@ import {
type Document,
type DocumentSpace,
DocumentState,
type DocumentMeta
type DocumentMeta,
type ProjectDocument,
type Project
} from '@hcengineering/controlled-documents'
import { type Resources } from '@hcengineering/platform'
import { type ObjectSearchResult, getClient, MessageBox } from '@hcengineering/presentation'
@@ -101,6 +103,7 @@ import {
createTemplate
} from './utils'
import { comment, isCommentVisible } from './text'
import TransferDocumentPopup from './components/document/popups/TransferDocumentPopup.svelte'
export { DocumentStatusTag, DocumentTitle, DocumentVersionPresenter, StatePresenter }
@@ -207,6 +210,46 @@ async function canArchiveDocument (obj?: Doc | Doc[]): Promise<boolean> {
).then((res) => res.every((r) => r))
}
async function canTransferDocument (obj?: Doc | Doc[]): Promise<boolean> {
if (obj == null) {
return false
}
const objs = (Array.isArray(obj) ? obj : [obj]) as Document[]
const spaces = new Set(objs.map((doc) => doc.space))
return await Promise.all(
Array.from(spaces).map(
async (space) => await checkPermission(getClient(), documents.permission.ArchiveDocument, space)
)
).then((res) => res.every((r) => r))
}
async function transferDocuments (selection: Document | Document[]): Promise<void> {
const objects = Array.isArray(selection) ? selection : [selection]
const client = getClient()
const h = client.getHierarchy()
let sourceDocumentIds: Array<Ref<DocumentMeta>> = []
let sourceSpaceId: Ref<DocumentSpace> | undefined
let sourceProjectId: Ref<Project<DocumentSpace>> | undefined
if (objects.length < 1) return
if (h.isDerived(objects[0]._class, documents.class.ProjectDocument)) {
const pjDocs = objects as unknown as ProjectDocument[]
const pjMeta = await client.findAll(documents.class.ProjectMeta, { _id: { $in: pjDocs.map((d) => d.attachedTo) } })
const docMeta = await client.findAll(documents.class.DocumentMeta, { _id: { $in: pjMeta.map((d) => d.meta) } })
sourceDocumentIds = docMeta.map((d) => d._id)
sourceSpaceId = pjDocs[0].space
sourceProjectId = pjDocs[0].project
}
if (sourceDocumentIds.length < 1) return
showPopup(TransferDocumentPopup, { sourceDocumentIds, sourceSpaceId, sourceProjectId })
}
async function isLatestDraftDoc (obj?: Doc | Doc[]): Promise<boolean> {
if (obj == null) {
return false
@@ -322,6 +365,7 @@ export default async (): Promise<Resources> => ({
GetDocumentMetaLinkFragment: getDocumentMetaLinkFragment,
CanDeleteDocument: canDeleteDocument,
CanArchiveDocument: canArchiveDocument,
CanTransferDocument: canTransferDocument,
DocumentIdentifierProvider: documentIdentifierProvider,
ControlledDocumentTitleProvider: getControlledDocumentTitle,
Comment: comment,
@@ -334,6 +378,7 @@ export default async (): Promise<Resources> => ({
CreateTemplate: createTemplate,
DeleteDocument: deleteDocuments,
ArchiveDocument: archiveDocuments,
TransferDocument: transferDocuments,
EditDocSpace: editDocSpace
},
resolver: {
@@ -238,6 +238,7 @@ export default mergeIds(documentsId, documents, {
GetDocumentMetaLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
CanDeleteDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanArchiveDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanTransferDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
}
})
+7 -1
View File
@@ -118,6 +118,7 @@ export const documentsPlugin = plugin(documentsId, {
DeleteDocument: '' as Ref<Action>,
ArchiveDocument: '' as Ref<Action>,
EditDocSpace: '' as Ref<Action>,
TransferDocument: '' as Ref<Action>,
Print: '' as Ref<Action<Doc, { signed: boolean }>>
},
function: {
@@ -259,7 +260,12 @@ export const documentsPlugin = plugin(documentsId, {
DeleteDocumentCategoryPermission: '' as IntlString,
DeleteDocumentCategoryDescription: '' as IntlString,
ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString
ConfigDescription: '' as IntlString,
Transfer: '' as IntlString,
TransferWarning: '' as IntlString,
TransferDocuments: '' as IntlString,
TransferDocumentsHint: '' as IntlString
},
ids: {
NoParent: '' as Ref<DocumentMeta>,
+367
View File
@@ -14,7 +14,10 @@
//
import {
ApplyOperations,
checkPermission,
Class,
Data,
Doc,
DocumentQuery,
DocumentUpdate,
Rank,
@@ -28,17 +31,23 @@ import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
import documents from './plugin'
import attachment, { Attachment } from '@hcengineering/attachment'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import tags, { TagReference } from '@hcengineering/tags'
import {
ChangeControl,
ControlledDocument,
Document,
DocumentMeta,
DocumentRequest,
DocumentSnapshot,
DocumentSpace,
DocumentState,
Project,
ProjectDocument,
ProjectMeta
} from './types'
import { makeRank } from '@hcengineering/rank'
/**
* @public
@@ -129,6 +138,364 @@ export async function deleteProjectDrafts (client: ApplyOperations, source: Ref<
}
}
class ProjectDocumentTree {
rootDocs: ProjectMeta[]
childrenByParent: Map<Ref<DocumentMeta>, ProjectMeta[]>
constructor (pjMeta: ProjectMeta[]) {
this.rootDocs = []
this.childrenByParent = new Map<Ref<DocumentMeta>, Array<ProjectMeta>>()
for (const meta of pjMeta) {
const parentId = meta.path[0] ?? documents.ids.NoParent
if (!this.childrenByParent.has(parentId)) {
this.childrenByParent.set(parentId, [])
}
this.childrenByParent.get(parentId)?.push(meta)
if (parentId === documents.ids.NoParent) {
this.rootDocs.push(meta)
}
}
}
getDescendants (parent: Ref<DocumentMeta>): Ref<DocumentMeta>[] {
const result: Ref<DocumentMeta>[] = []
const queue: Ref<DocumentMeta>[] = [parent]
while (queue.length > 0) {
const next = queue.pop()
if (next === undefined) break
const children = this.childrenByParent.get(next) ?? []
const childrenRefs = children.map((p) => p.meta)
result.push(...childrenRefs)
queue.push(...childrenRefs)
}
return result
}
}
export async function findProjectDocsHierarchy (
client: TxOperations,
space: Ref<DocumentSpace>,
project?: Ref<Project<DocumentSpace>>
): Promise<ProjectDocumentTree> {
const pjMeta = await client.findAll(documents.class.ProjectMeta, { space, project })
return new ProjectDocumentTree(pjMeta)
}
export interface DocumentBundle {
DocumentMeta: DocumentMeta[]
ProjectMeta: ProjectMeta[]
ProjectDocument: ProjectDocument[]
ControlledDocument: ControlledDocument[]
ChangeControl: ChangeControl[]
DocumentRequest: DocumentRequest[]
DocumentSnapshot: DocumentSnapshot[]
ChatMessage: ChatMessage[]
TagReference: TagReference[]
Attachment: Attachment[]
}
function emptyBundle (): DocumentBundle {
return {
DocumentMeta: [],
ProjectMeta: [],
ProjectDocument: [],
ControlledDocument: [],
ChangeControl: [],
DocumentRequest: [],
DocumentSnapshot: [],
ChatMessage: [],
TagReference: [],
Attachment: []
}
}
export async function findAllDocumentBundles (
client: TxOperations,
ids: Ref<DocumentMeta>[]
): Promise<DocumentBundle[]> {
const all: DocumentBundle = { ...emptyBundle() }
async function crawl<T extends Doc, P extends keyof T> (
_class: Ref<Class<T>>,
bkey: keyof DocumentBundle,
prop: P,
ids: T[P][]
): Promise<T[]> {
const data = await client.findAll(_class, { [prop]: { $in: ids } } as any)
all[bkey].push(...(data as any))
return data
}
await crawl(documents.class.DocumentMeta, 'DocumentMeta', '_id', ids)
await crawl(
documents.class.ProjectMeta,
'ProjectMeta',
'meta',
all.DocumentMeta.map((m) => m._id)
)
await crawl(
documents.class.ProjectDocument,
'ProjectDocument',
'attachedTo',
all.ProjectMeta.map((m) => m._id)
)
await crawl(
documents.class.ControlledDocument,
'ControlledDocument',
'attachedTo',
all.DocumentMeta.map((m) => m._id)
)
await crawl(
documents.class.ChangeControl,
'ChangeControl',
'_id',
all.ControlledDocument.map((p) => p.changeControl)
)
await crawl(
documents.class.DocumentRequest,
'DocumentRequest',
'attachedTo',
all.ControlledDocument.map((p) => p._id)
)
await crawl(
documents.class.DocumentSnapshot,
'DocumentSnapshot',
'attachedTo',
all.ControlledDocument.map((p) => p._id)
)
await crawl(
documents.class.DocumentComment,
'ChatMessage',
'attachedTo',
all.ControlledDocument.map((p) => p._id)
)
await crawl(
chunter.class.ThreadMessage,
'ChatMessage',
'attachedTo',
all.ChatMessage.map((p) => p._id)
)
await crawl(
tags.class.TagReference,
'TagReference',
'attachedTo',
all.ControlledDocument.map((p) => p._id)
)
await crawl(attachment.class.Attachment, 'Attachment', 'attachedTo', [
...all.ChatMessage.map((p) => p._id),
...all.ControlledDocument.map((p) => p._id)
])
const bundles = new Map<Ref<DocumentMeta>, DocumentBundle>(all.DocumentMeta.map((m) => [m._id, { ...emptyBundle() }]))
const links = new Map<Ref<Doc>, Ref<DocumentMeta>>()
const link = (ref: Ref<Doc>, lookup: Ref<Doc>): void => {
const meta = links.get(lookup)
if (meta !== undefined) links.set(ref, meta)
}
const relink = (ref: Ref<Doc>, prop: keyof DocumentBundle, obj: DocumentBundle[typeof prop][0]): void => {
const meta = links.get(ref)
if (meta !== undefined) bundles.get(meta)?.[prop].push(obj as any)
}
for (const m of all.DocumentMeta) links.set(m._id, m._id) // DocumentMeta -> DocumentMeta
for (const m of all.ProjectMeta) links.set(m._id, m.meta) // ProjectMeta -> DocumentMeta
for (const m of all.ProjectDocument) {
link(m._id, m.attachedTo) // ProjectDocument -> ProjectMeta
link(m.document, m.attachedTo) // ControlledDocument -> ProjectMeta
}
for (const m of all.ControlledDocument) link(m.changeControl, m.attachedTo) // ChangeControl -> ControlledDocument
for (const m of all.DocumentRequest) link(m._id, m.attachedTo) // DocumentRequest -> ControlledDocument
for (const m of all.DocumentSnapshot) link(m._id, m.attachedTo) // DocumentSnapshot -> ControlledDocument
for (const m of all.ChatMessage) link(m._id, m.attachedTo) // ChatMessage -> (ControlledDocument | ChatMessage)
for (const m of all.TagReference) link(m._id, m.attachedTo) // TagReference -> ControlledDocument
for (const m of all.Attachment) link(m._id, m.attachedTo) // Attachment -> (ControlledDocument | ChatMessage)
let key: keyof DocumentBundle
for (key in all) {
all[key].forEach((value) => {
relink(value._id, key, value)
})
}
return Array.from(bundles.values())
}
export async function findOneDocumentBundle (
client: TxOperations,
id: Ref<DocumentMeta>
): Promise<DocumentBundle | undefined> {
const bundles = await findAllDocumentBundles(client, [id])
return bundles[0]
}
export interface DocumentTransferRequest {
sourceDocumentIds: Ref<DocumentMeta>[]
sourceSpaceId: Ref<DocumentSpace>
sourceProjectId?: Ref<Project<DocumentSpace>>
targetSpaceId: Ref<DocumentSpace>
targetParentId?: Ref<DocumentMeta>
targetProjectId?: Ref<Project<DocumentSpace>>
}
interface DocumentTransferContext {
request: DocumentTransferRequest
bundles: DocumentBundle[]
sourceTree: ProjectDocumentTree
targetTree: ProjectDocumentTree
sourceSpace: DocumentSpace
targetSpace: DocumentSpace
targetParentBundle?: DocumentBundle
}
async function _buildDocumentTransferContext (
client: TxOperations,
request: DocumentTransferRequest
): Promise<DocumentTransferContext | undefined> {
const sourceTree = await findProjectDocsHierarchy(client, request.sourceSpaceId, request.sourceProjectId)
const targetTree = await findProjectDocsHierarchy(client, request.targetSpaceId, request.targetProjectId)
const docIds = new Set<Ref<DocumentMeta>>(request.sourceDocumentIds)
for (const id of request.sourceDocumentIds) {
sourceTree.getDescendants(id).forEach((d) => docIds.add(d))
}
const bundles = await findAllDocumentBundles(client, Array.from(docIds))
const targetParentBundle =
request.targetParentId !== undefined ? await findOneDocumentBundle(client, request.targetParentId) : undefined
const sourceSpace = await client.findOne(documents.class.DocumentSpace, { _id: request.sourceSpaceId })
const targetSpace = await client.findOne(documents.class.DocumentSpace, { _id: request.targetSpaceId })
if (sourceSpace === undefined || targetSpace === undefined) return
return {
request,
bundles,
sourceTree,
targetTree,
sourceSpace,
targetSpace,
targetParentBundle
}
}
export async function listDocumentsAffectedByTransfer (
client: TxOperations,
req: DocumentTransferRequest
): Promise<DocumentMeta[]> {
const cx = await _buildDocumentTransferContext(client, req)
return cx?.bundles.map((b) => b.DocumentMeta[0]) ?? []
}
/**
* @public
*/
export async function canTransferDocuments (client: TxOperations, req: DocumentTransferRequest): Promise<boolean> {
const cx = await _buildDocumentTransferContext(client, req)
return cx !== undefined ? await _transferDocuments(client, cx, 'check') : false
}
/**
* @public
*/
export async function transferDocuments (client: TxOperations, req: DocumentTransferRequest): Promise<boolean> {
const cx = await _buildDocumentTransferContext(client, req)
return cx !== undefined ? await _transferDocuments(client, cx) : false
}
async function _transferDocuments (
client: TxOperations,
cx: DocumentTransferContext,
mode: 'default' | 'check' = 'default'
): Promise<boolean> {
if (cx.bundles.length < 1) return false
if (cx.targetSpace._id === cx.sourceSpace._id) return false
const hierarchy = client.getHierarchy()
const canArchiveInSourceSpace = await checkPermission(
client,
documents.permission.ArchiveDocument,
cx.request.sourceSpaceId
)
const canCreateInTargetSpace = await checkPermission(
client,
documents.permission.CreateDocument,
cx.request.targetSpaceId
)
if (!canArchiveInSourceSpace || !canCreateInTargetSpace) return false
for (const bundle of cx.bundles) {
if (bundle.DocumentMeta.length !== 1) return false
if (bundle.ProjectMeta.length !== 1) return false
if (bundle.DocumentMeta[0].space !== cx.request.sourceSpaceId) return false
if (bundle.ControlledDocument.length < 1) return false
const isTemplate = hierarchy.hasMixin(bundle.ControlledDocument[0], documents.mixin.DocumentTemplate)
if (isTemplate && hierarchy.isDerived(cx.targetSpace._class, documents.class.ExternalSpace)) return false
}
const roots = new Set(cx.request.sourceDocumentIds)
const updates = new Map<Doc, Partial<Doc>>()
function update<T extends Doc> (document: T, update: Partial<T>): void {
updates.set(document, { ...updates.get(document), ...update })
}
const parentMeta = cx.targetParentBundle?.ProjectMeta[0]
const project = cx.request.targetProjectId ?? documents.ids.NoProject
if (cx.targetParentBundle !== undefined && parentMeta === undefined) return false
let lastRank: Rank | undefined
if (parentMeta !== undefined) {
lastRank = await getFirstRank(client, cx.targetSpace._id, project, parentMeta.meta)
}
for (const bundle of cx.bundles) {
const projectMeta = bundle.ProjectMeta[0]
if (roots.has(projectMeta.meta)) {
const path = parentMeta?.path !== undefined ? [parentMeta.meta, ...parentMeta.path] : []
const parent = path[0] ?? documents.ids.NoParent
const rank = makeRank(lastRank, undefined)
update(projectMeta, { parent, path, rank })
}
let key: keyof DocumentBundle
for (key in bundle) {
bundle[key].forEach((doc) => {
update(doc, { space: cx.targetSpace._id })
})
}
for (const m of bundle.ProjectMeta) update(m, { project })
for (const m of bundle.ProjectDocument) update(m, { project })
}
if (mode === 'check') return true
const ops = client.apply()
for (const u of updates) await ops.update(u[0], u[1])
const commit = await ops.commit()
return commit.result
}
/**
* @public
*/
@@ -387,7 +387,7 @@
$: activeParticipants = getActiveParticipants(participants)
</script>
<div bind:this={roomEl} class="flex-col-center w-full h-full right-navpanel-border" class:theme-dark={$isFullScreen}>
<div bind:this={roomEl} class="flex-col-center w-full h-full" class:theme-dark={$isFullScreen}>
{#if $isConnected && !$isCurrentInstanceConnected}
<div class="flex justify-center error h-full w-full clear-mins">
<Label label={love.string.AnotherWindowError} />
@@ -18,7 +18,7 @@
import { type Person, formatName } from '@hcengineering/contact'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { IconSize, tooltip } from '@hcengineering/ui'
import { IconSize, tooltip, deviceOptionsStore as deviceInfo, checkAdaptiveMatching } from '@hcengineering/ui'
import PresenceList from './PresenceList.svelte'
import { presenceByObjectId } from '../store'
@@ -33,10 +33,11 @@
.map((p) => $personByIdStore.get(p))
.filter((p): p is Person => p !== undefined)
$: overLimit = persons.length > limit
$: adaptive = checkAdaptiveMatching($deviceInfo.size, 'md') || overLimit
</script>
{#if persons.length > 0}
{#if overLimit}
{#if adaptive}
<div
class="hulyCombineAvatars-container"
use:tooltip={{ component: PresenceList, props: { persons, size }, direction: 'bottom' }}
@@ -18,7 +18,7 @@
import type { Contact, Employee, Person } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import { EmployeeBox, ExpandRightDouble, UserBox } from '@hcengineering/contact-resources'
import {
import core, {
Account,
AccountRole,
Class,
@@ -160,10 +160,7 @@
status: selectedState._id,
number,
identifier: `APP-${number}`,
assignee: doc.assignee,
rank: makeRank(lastOne?.rank, undefined),
startDate: null,
dueDate: null,
kind
},
doc._id
@@ -430,7 +427,7 @@
<InlineAttributeBar
_class={recruit.class.Applicant}
object={doc}
toClass={task.class.Task}
toClass={core.class.AttachedDoc}
ignoreKeys={['assignee', 'status']}
extraProps={{ showNavigate: false, space: vacancy._id }}
/>
@@ -136,7 +136,6 @@
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: settingPlg.ids.SettingsWidget })[0]
$: if (moveASide && asideComponent != null && $sidebarStore.widget !== widget._id) {
openWidget(widget, { component: asideComponent, ...asideProps }, { active: true, openedByUser: true })
$deviceInfo.aside.visible = true
} else if (moveASide && asideComponent == null && $sidebarStore.widget === widget._id) {
closeWidget(widget._id)
minimizeSidebar()
@@ -10,6 +10,7 @@
export let size: ButtonSize = 'medium'
export let kind: ButtonKind = 'link'
export let editable: boolean = true
export let onChange: ((value: any) => void) | undefined
const client = getClient()
$: status = $statusStore.byId.get(object.status)
@@ -21,15 +22,19 @@
return
}
await client.updateCollection(
object._class,
object.space,
object._id,
object.attachedTo,
object.attachedToClass,
object.collection,
{ dueDate: newDueDate }
)
if (onChange !== undefined) {
onChange(newDueDate)
} else {
await client.updateCollection(
object._class,
object.space,
object._id,
object.attachedTo,
object.attachedToClass,
object.collection,
{ dueDate: newDueDate }
)
}
}
</script>
@@ -83,7 +83,7 @@
import { type FileAttachFunction } from './extension/types'
import { completionConfig, inlineCommandsConfig } from './extensions'
import { mermaidOptions } from './extension/mermaid'
import { InlineCommentExtension } from './extension/inlineComment'
import { InlineCommentCollaborationExtension } from './extension/inlineComment'
export let object: Doc
export let attribute: KeyedAttribute
@@ -435,7 +435,12 @@
if (enableInlineComments) {
optionalExtensions.push(
InlineCommentExtension.configure({ ydoc, boundary, popupContainer: editorPopupContainer, requestSideSpace })
InlineCommentCollaborationExtension.configure({
ydoc,
boundary,
popupContainer: editorPopupContainer,
requestSideSpace
})
)
}
@@ -36,6 +36,8 @@ export const codeBlockHighlightOptions: CodeBlockLowlightOptions = {
}
export const CodeBlockHighlighExtension = CodeBlockLowlight.extend<CodeBlockLowlightOptions>({
marks: 'inline-comment',
addCommands () {
return {
setCodeBlock:
@@ -33,7 +33,7 @@ function colorVar (tag: string, prefix = 'text'): string {
function colorSpec (tag: string, prefix = 'text'): ColorSpec {
const color = colorVar(tag, prefix)
return { color, preview: colorVar(tag) }
return { color }
}
const palette = {
@@ -13,8 +13,20 @@
// limitations under the License.
//
import chunter from '@hcengineering/chunter'
import core, {
type Account,
type Markup,
type Ref,
type Timestamp,
generateId,
getCurrentAccount
} from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { type Editor, Mark } from '@tiptap/core'
import { type ActionContext } from '@hcengineering/presentation'
import type { AnySvelteComponent } from '@hcengineering/ui'
import { type Editor, Extension } from '@tiptap/core'
import { type Node } from '@tiptap/pm/model'
import {
type EditorState,
Plugin,
@@ -25,16 +37,10 @@ import {
type Transaction
} from '@tiptap/pm/state'
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
import { SvelteRenderer } from '../node-view'
import type { AnySvelteComponent } from '@hcengineering/ui'
import { Fragment, Slice, type Node } from '@tiptap/pm/model'
import { type Account, type Markup, type Ref, type Timestamp, getCurrentAccount, generateId } from '@hcengineering/core'
import tippy, { type Instance } from 'tippy.js'
import 'tippy.js/animations/shift-toward.css'
import { type Doc as YDoc, type Map as YMap } from 'yjs'
import core from '@hcengineering/core'
import { type ActionContext } from '@hcengineering/presentation'
import chunter from '@hcengineering/chunter'
import { SvelteRenderer } from '../node-view'
interface InlineCommentExtensionOptions {
boundary?: HTMLElement
@@ -111,41 +117,10 @@ interface ThreadPresenterProps {
handleResolveThread?: (() => void) | undefined
}
const extensionName = 'inline-comment'
export const InlineCommentExtension = Mark.create<InlineCommentExtensionOptions>({
name: 'inline-comment',
excludes: '',
inclusive: false,
parseHTML () {
return [
{
tag: 'span.proseInlineComment[data-inline-comment-thread]'
}
]
},
renderHTML ({ HTMLAttributes, mark }) {
return ['span', { ...HTMLAttributes, class: 'proseInlineComment' }, 0]
},
addAttributes () {
const name = 'data-inline-comment-thread-id'
return {
thread: {
default: undefined,
parseHTML: (element) => {
return element.getAttribute(name)
},
renderHTML: (attributes) => {
return { [name]: attributes.thread }
}
}
}
},
const extensionName = 'inlineCommentCollaboration'
export const InlineCommentCollaborationExtension = Extension.create<InlineCommentExtensionOptions>({
name: extensionName,
addProseMirrorPlugins () {
return [...(this.parent?.() ?? []), InlineCommentDecorator(this.options)]
}
@@ -180,13 +155,6 @@ export function InlineCommentDecorator (options: InlineCommentExtensionOptions):
},
handleDOMEvents: {
mousemove: handleInlineCommentMouseHover
},
transformPasted: (slice) => {
const nodes: Node[] = []
slice.content.forEach((node) => {
nodes.push(removeMarkFromNode(node, 'inline-comment'))
})
return new Slice(Fragment.fromArray(nodes), slice.openStart, slice.openEnd)
}
},
state: {
@@ -491,22 +459,6 @@ function handleInlineCommentMouseHover (view: EditorView, event: MouseEvent): vo
updatePointerState(view, { hover: threadIds })
}
function removeMarkFromNode (node: Node, name: string): Node {
if (node.isText) {
return node.mark(node.marks.filter((mark) => mark.type.name !== name))
}
if (node.content.size > 0) {
const nodes: Node[] = []
node.content.forEach((child) => {
nodes.push(removeMarkFromNode(child, name))
})
return node.copy(Fragment.fromArray(nodes))
}
return node
}
interface InlineCommentViewProps {
siblings: InlineCommentView[]
thread: Thread
@@ -71,6 +71,7 @@ interface NodePatchSpec {
export const MermaidExtension = CodeBlockLowlight.extend<MermaidOptions>({
name: 'mermaid',
group: 'block',
marks: 'inline-comment',
draggable: true,
selectable: true,
@@ -59,7 +59,8 @@
position: relative;
width: 1.5rem;
height: 1.5rem;
border: 1px solid var(--theme-button-border);
border-radius: 0.25rem;
cursor: pointer;
box-shadow: var(--text-editor-color-picker-outline) 0px 0px 0px 1px inset;
}
</style>
@@ -15,7 +15,14 @@
import { type Class, type Doc, type Ref, type Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getBlobRef, getClient } from '@hcengineering/presentation'
import { BackgroundColor, CodeExtension, codeOptions, TextColor, TextStyle } from '@hcengineering/text'
import {
BackgroundColor,
CodeExtension,
codeOptions,
InlineCommentMark,
TextColor,
TextStyle
} from '@hcengineering/text'
import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor'
import { type AnyExtension, type Editor, Extension } from '@tiptap/core'
import { type Level } from '@tiptap/extension-heading'
@@ -182,6 +189,7 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
})
],
[110, EditableExtension],
[150, InlineCommentMark.configure({})],
[200, CodeBlockHighlighExtension.configure(codeBlockHighlightOptions)],
[210, CodeExtension.configure(codeOptions)],
[220, HardBreakExtension.configure({ shortcuts: mode })]
+11 -2
View File
@@ -9,7 +9,14 @@ import {
getCurrentAccount
} from '@hcengineering/core'
import { type Asset, type IntlString, type Resource, getResource } from '@hcengineering/platform'
import { MessageBox, getClient, updateAttribute, type ContextStore, contextStore } from '@hcengineering/presentation'
import {
MessageBox,
getClient,
updateAttribute,
type ContextStore,
contextStore,
copyTextToClipboardOldBrowser
} from '@hcengineering/presentation'
import {
type AnyComponent,
type AnySvelteComponent,
@@ -73,7 +80,9 @@ async function CopyTextToClipboard (
const text = Array.isArray(doc)
? (await Promise.all(doc.map(async (d) => await getText(d, props.props)))).join(',')
: await getText(doc, props.props)
await navigator.clipboard.writeText(text)
if (navigator.clipboard != null && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(text)
} else copyTextToClipboardOldBrowser(text)
}
}
@@ -159,19 +159,12 @@
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const mobileAdaptive = $deviceInfo.isMobile && $deviceInfo.minWidth
const defaultNavigator = !(getMetadata(workbench.metadata.NavigationExpandedDefault) ?? true)
const savedNavigator = localStorage.getItem('hiddenNavigator')
const savedAside = localStorage.getItem('hiddenAside')
let hiddenNavigator: boolean = savedNavigator !== null ? savedNavigator === 'true' : defaultNavigator
let hiddenAside: boolean = savedAside !== null ? savedAside === 'true' : defaultNavigator
let hiddenAside: boolean = true
$deviceInfo.navigator.visible = !hiddenNavigator
$deviceInfo.aside.visible = !hiddenAside
sidebarStore.subscribe((sidebar) => {
if (!$deviceInfo.aside.float) {
hiddenAside = sidebar.variant === SidebarVariant.MINI
localStorage.setItem('hiddenAside', `${hiddenAside}`)
}
})
async function toggleNav (): Promise<void> {
$deviceInfo.navigator.visible = !$deviceInfo.navigator.visible
@@ -642,37 +635,42 @@
}
}
checkWorkbenchWidth()
$: if ($deviceInfo.docWidth <= FLOAT_ASIDE && !$deviceInfo.aside.float) {
$deviceInfo.aside.visible = false
$deviceInfo.aside.float = true
} else if ($deviceInfo.docWidth > FLOAT_ASIDE && $deviceInfo.aside.float) {
$deviceInfo.aside.float = false
$deviceInfo.aside.visible = !hiddenAside
$: if ($deviceInfo.docWidth <= FLOAT_ASIDE && !$sidebarStore.float) {
hiddenAside = $sidebarStore.variant === SidebarVariant.MINI
$sidebarStore.float = true
} else if ($deviceInfo.docWidth > FLOAT_ASIDE && $sidebarStore.float) {
$sidebarStore.float = false
$sidebarStore.variant = hiddenAside ? SidebarVariant.MINI : SidebarVariant.EXPANDED
}
const checkOnHide = (): void => {
if ($deviceInfo.navigator.visible && $deviceInfo.navigator.float) $deviceInfo.navigator.visible = false
}
let oldNavVisible: boolean = $deviceInfo.navigator.visible
let oldASideVisible: boolean = $deviceInfo.aside.visible
$: if (oldNavVisible !== $deviceInfo.navigator.visible || oldASideVisible !== $deviceInfo.aside.visible) {
if ($deviceInfo.isMobile && $deviceInfo.isPortrait && $deviceInfo.navigator.float) {
if ($deviceInfo.navigator.visible && $deviceInfo.aside.visible) {
let oldASideVisible: boolean = $sidebarStore.variant !== SidebarVariant.MINI
$: if (
oldNavVisible !== $deviceInfo.navigator.visible ||
oldASideVisible !== ($sidebarStore.variant !== SidebarVariant.MINI)
) {
if (mobileAdaptive && $deviceInfo.navigator.float) {
if ($deviceInfo.navigator.visible && $sidebarStore.variant !== SidebarVariant.MINI) {
if (oldNavVisible) $deviceInfo.navigator.visible = false
else $deviceInfo.aside.visible = false
else $sidebarStore.variant = SidebarVariant.MINI
}
}
oldNavVisible = $deviceInfo.navigator.visible
oldASideVisible = $deviceInfo.aside.visible
oldASideVisible = $sidebarStore.variant !== SidebarVariant.MINI
}
$: if (
$deviceInfo.aside.float &&
$deviceInfo.aside.visible &&
$sidebarStore.variant === SidebarVariant.MINI &&
$sidebarStore.float &&
$sidebarStore.variant !== SidebarVariant.MINI &&
$sidebarStore.widget === undefined &&
$sidebarStore.widgetsState.size > 0
) {
$sidebarStore.variant = SidebarVariant.EXPANDED
$sidebarStore.widget = Array.from($sidebarStore.widgetsState.keys())[0]
}
location.subscribe(() => {
if (mobileAdaptive && $sidebarStore.variant !== SidebarVariant.MINI) $sidebarStore.variant = SidebarVariant.MINI
})
$: $deviceInfo.navigator.direction = $deviceInfo.isMobile && $deviceInfo.isPortrait ? 'horizontal' : 'vertical'
let appsMini: boolean
$: appsMini =
@@ -976,9 +974,9 @@
<div
bind:this={contentPanel}
class={navigatorModel === undefined ? 'hulyPanels-container' : 'hulyComponent overflow-hidden'}
class:straighteningCorners={$deviceInfo.aside.float &&
class:straighteningCorners={$sidebarStore.float &&
$sidebarStore.variant === SidebarVariant.EXPANDED &&
!($deviceInfo.isMobile && $deviceInfo.isPortrait && $deviceInfo.minWidth)}
!(mobileAdaptive && $deviceInfo.isPortrait)}
data-id={'contentPanel'}
>
{#if currentApplication && currentApplication.component}
@@ -1022,7 +1020,7 @@
{/if}
</div>
</div>
{#if $sidebarStore.variant === SidebarVariant.EXPANDED && !$deviceInfo.aside.float}
{#if $sidebarStore.variant === SidebarVariant.EXPANDED && !$sidebarStore.float}
<Separator name={'main'} index={0} color={'transparent'} separatorSize={0} short />
{/if}
<WidgetsBar />
@@ -61,7 +61,7 @@
})
</script>
<div id="sidebar" class="antiPanel-application vertical sidebar-container" class:mini={mini || $deviceInfo.aside.float}>
<div id="sidebar" class="antiPanel-application vertical sidebar-container" class:mini={mini || $sidebarStore.float}>
{#if mini}
<SidebarMini {widgets} {preferences} />
{:else if $sidebarStore.variant === SidebarVariant.EXPANDED}
@@ -54,7 +54,7 @@
$: if ($sidebarStore.widget === undefined) {
sidebarStore.update((s) => ({ ...s, variant: SidebarVariant.MINI }))
}
$: float = $deviceInfo.aside.float
$: float = $sidebarStore.float
function closeWrongTabs (loc: Location): void {
if (widget === undefined) return
@@ -99,7 +99,7 @@
}
</script>
<div class="sidebar-wrap__content" class:float>
<div class="sidebar-wrap__content{float ? ` float apps-${$deviceInfo.navigator.direction}` : ''}">
{#if float && !($deviceInfo.isMobile && $deviceInfo.isPortrait && $deviceInfo.minWidth)}
<Separator name={'main'} index={0} color={'var(--theme-navpanel-border)'} float={'sidebar'} />
{/if}
@@ -178,10 +178,16 @@
:global(.mobile-theme) & {
overflow: hidden;
height: calc(100% - var(--app-panel-width));
border: 1px solid var(--theme-divider-color);
border-radius: var(--medium-BorderRadius);
filter: var(--theme-navpanel-shadow-mobile);
&.apps-horizontal {
height: calc(100% - var(--app-panel-width));
}
:global(.antiSeparator) {
display: none;
}
}
}
}
+10 -9
View File
@@ -39,12 +39,14 @@ export interface WidgetState {
export interface SidebarState {
variant: SidebarVariant
float: boolean
widgetsState: Map<Ref<Widget>, WidgetState>
widget?: Ref<Widget>
}
export const defaultSidebarState: SidebarState = {
variant: SidebarVariant.MINI,
float: false,
widgetsState: new Map()
}
@@ -75,10 +77,12 @@ function getSidebarStateFromLocalStorage (workspace: string): SidebarState {
try {
const parsed = JSON.parse(state)
const device = get(deviceInfo)
return {
...defaultSidebarState,
...parsed,
variant: device.isMobile && device.minWidth ? SidebarVariant.MINI : parsed.variant ?? defaultSidebarState.variant,
widgetsState: new Map(Object.entries(parsed.widgetsState ?? {}))
}
} catch (e) {
@@ -94,9 +98,14 @@ function setSidebarStateToLocalStorage (state: SidebarState): void {
const sidebarStateLocalStorageKey = getSideBarLocalStorageKey(workspace)
if (sidebarStateLocalStorageKey === undefined) return
const device = get(deviceInfo)
window.localStorage.setItem(
sidebarStateLocalStorageKey,
JSON.stringify({ ...state, widgetsState: Object.fromEntries(state.widgetsState.entries()) })
JSON.stringify({
...state,
variant: device.isMobile && device.minWidth ? SidebarVariant.MINI : state.variant,
widgetsState: Object.fromEntries(state.widgetsState.entries())
})
)
}
@@ -255,10 +264,6 @@ export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false)
widgetsState,
variant: SidebarVariant.EXPANDED
})
const devInfo = get(deviceInfo)
if (devInfo.aside.float && !devInfo.aside.visible) {
deviceInfo.set({ ...devInfo, aside: { visible: true, float: true } })
}
}
export function pinWidgetTab (widget: Widget, tabId: string): void {
@@ -338,10 +343,6 @@ export function minimizeSidebar (closedByUser = false): void {
}
sidebarStore.set({ ...state, ...widgetsState, widget: undefined, variant: SidebarVariant.MINI })
const devInfo = get(deviceInfo)
if (devInfo.aside.float && devInfo.aside.visible) {
deviceInfo.set({ ...devInfo, aside: { visible: false, float: true } })
}
}
export function updateTabData (widget: Ref<Widget>, tabId: string, data: Record<string, any>): void {
+2 -2
View File
@@ -90,9 +90,9 @@ export class DatalakeClient {
async listObjects (
ctx: MeasureContext,
workspace: WorkspaceId,
cursor: string | undefined
cursor: string | undefined,
limit: number = 100
): Promise<ListObjectOutput> {
const limit = 100
const path = `/blob/${workspace.name}`
const url = new URL(concatLink(this.endpoint, path))
url.searchParams.append('limit', String(limit))
+1
View File
@@ -141,6 +141,7 @@ export class CommonPage {
async pressYesDeletePopup (page: Page): Promise<void> {
await this.viewStringDeleteObjectButtonPrimary().click()
await expect(this.viewStringDeleteObjectButtonPrimary()).not.toBeVisible({ timeout: 1000 })
}
async addNewTagPopup (page: Page, title: string, description: string): Promise<void> {
@@ -78,6 +78,9 @@ export class ApplicationsPage extends CommonRecruitingPage {
.locator('tr', { hasText: `${talentName.lastName} ${talentName.firstName}` })
.locator('div[class*="firstCell"]')
.click()
await expect(
this.page.locator('div.hulyHeader-container div.hulyHeader-titleGroup', { hasText: talentName.lastName })
).toBeVisible({ timeout: 1000 })
}
async checkApplicationState (talentName: TalentName, done: string): Promise<void> {