mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-23 18:15:03 +02:00
Merge remote-tracking branch 'origin/develop' into staging
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
+25
-5
@@ -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
@@ -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]}`
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -37,6 +37,8 @@ export const backtickInputRegex = /^```$/
|
||||
export const tildeInputRegex = /^~~~$/
|
||||
|
||||
export const CodeBlockExtension = CodeBlock.extend({
|
||||
marks: 'inline-comment',
|
||||
|
||||
addAttributes () {
|
||||
return {
|
||||
language: {
|
||||
|
||||
@@ -24,6 +24,7 @@ export const mermaidOptions: CodeBlockOptions = {
|
||||
export const MermaidExtension = CodeBlock.extend({
|
||||
name: 'mermaid',
|
||||
group: 'block',
|
||||
marks: 'inline-comment',
|
||||
|
||||
parseHTML () {
|
||||
return [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 |
@@ -105,6 +105,8 @@
|
||||
"For": "Pro",
|
||||
"SelectUsers": "Vyberte uživatele",
|
||||
"AddGuest": "Přidat hosta",
|
||||
"ViewProfile": "Zobrazit profil"
|
||||
"ViewProfile": "Zobrazit profil",
|
||||
"Viber": "Viber",
|
||||
"ViberPlaceholder": "Viber"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"For": "For",
|
||||
"SelectUsers": "Select users",
|
||||
"AddGuest": "Add guest",
|
||||
"ViewProfile": "View profile"
|
||||
"ViewProfile": "View profile",
|
||||
"Viber": "Viber",
|
||||
"ViberPlaceholder": "Viber"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"For": "Para",
|
||||
"SelectUsers": "Seleccionar usuarios",
|
||||
"AddGuest": "Añadir invitado",
|
||||
"ViewProfile": "Ver perfil"
|
||||
"ViewProfile": "Ver perfil",
|
||||
"Viber": "Viber",
|
||||
"ViberPlaceholder": "Viber"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"For": "Per",
|
||||
"SelectUsers": "Seleziona utenti",
|
||||
"AddGuest": "Aggiungi ospite",
|
||||
"ViewProfile": "Visualizza profilo"
|
||||
"ViewProfile": "Visualizza profilo",
|
||||
"Viber": "Viber",
|
||||
"ViberPlaceholder": "Viber"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"For": "Para",
|
||||
"SelectUsers": "Selecionar utilizadores",
|
||||
"AddGuest": "Adicionar convidado",
|
||||
"ViewProfile": "Ver perfil"
|
||||
"ViewProfile": "Ver perfil",
|
||||
"Viber": "Viber",
|
||||
"ViberPlaceholder": "Viber"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"For": "Для",
|
||||
"SelectUsers": "Выберите пользователей",
|
||||
"AddGuest": "Добавить гостя",
|
||||
"ViewProfile": "Посмотреть профиль"
|
||||
"ViewProfile": "Посмотреть профиль",
|
||||
"Viber": "Viber",
|
||||
"ViberPlaceholder": "Viber"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"For": "为",
|
||||
"SelectUsers": "选择用户",
|
||||
"AddGuest": "添加访客",
|
||||
"ViewProfile": "查看资料"
|
||||
"ViewProfile": "查看资料",
|
||||
"Viber": "Viber",
|
||||
"ViberPlaceholder": "Viber"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
+284
@@ -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>>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 })]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user