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

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2024-10-02 18:02:06 +07:00
51 changed files with 845 additions and 157 deletions
+13 -6
View File
@@ -53,9 +53,15 @@ function publish (name) {
function main () {
const args = process.argv
const version = args[2]
const doPublish = args.includes('--publish')
const version = args.reverse().shift()
if (version === undefined || version === '') {
console.log('usage: node bump.js <version>')
console.log('usage: node bump.js [--publish] <version>')
return
}
if( !/^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(version)) {
console.log('Invalid <version>', version, ' should be xx.xx.xx')
return
}
@@ -76,10 +82,11 @@ function main () {
const res = JSON.stringify(jsons[packageName], undefined, 2)
fs.writeFileSync(file, res + '\n')
}
for (const packageName of packageNames) {
if (shouldPublish(packageName)) {
publish(packageName)
if(doPublish) {
for (const packageName of packageNames) {
if (shouldPublish(packageName)) {
publish(packageName)
}
}
}
+1
View File
@@ -61,6 +61,7 @@
"@hcengineering/platform": "^0.6.11",
"@hcengineering/server-tool": "^0.6.0",
"@hcengineering/server-client": "^0.6.0",
"@hcengineering/rank": "^0.6.4",
"commander": "^8.1.0",
"mime-types": "~2.1.34"
}
+12 -3
View File
@@ -22,7 +22,8 @@ import {
collaborativeDocParse
} from '@hcengineering/core'
import { yDocToBuffer } from '@hcengineering/collaboration'
import document, { type Document, type Teamspace } from '@hcengineering/document'
import document, { type Document, type Teamspace, getFirstRank } from '@hcengineering/document'
import { makeRank } from '@hcengineering/rank'
import {
MarkupMarkType,
type MarkupNode,
@@ -328,6 +329,9 @@ async function createDBPageWithAttachments (
const parentId = parentMeta !== undefined ? (parentMeta.id as Ref<Document>) : document.ids.NoParent
const lastRank = await getFirstRank(client, space, parentId)
const rank = makeRank(lastRank, undefined)
const object: AttachedData<Document> = {
name: docMeta.name,
content: collabId,
@@ -336,7 +340,8 @@ async function createDBPageWithAttachments (
embeddings: 0,
labels: 0,
comments: 0,
references: 0
references: 0,
rank
}
await client.addCollection(
@@ -479,6 +484,9 @@ async function importPageDocument (
const parentId = parentMeta?.id ?? document.ids.NoParent
const lastRank = await getFirstRank(client, space, parentId as Ref<Document>)
const rank = makeRank(lastRank, undefined)
const attachedData: AttachedData<Document> = {
name: docMeta.name,
content: collabId,
@@ -487,7 +495,8 @@ async function importPageDocument (
embeddings: 0,
labels: 0,
comments: 0,
references: 0
references: 0,
rank
}
await client.addCollection(
+2 -1
View File
@@ -50,6 +50,7 @@
"@hcengineering/time": "^0.6.0",
"@hcengineering/document": "^0.6.0",
"@hcengineering/document-resources": "^0.6.0",
"@hcengineering/collaboration": "^0.6.0"
"@hcengineering/collaboration": "^0.6.0",
"@hcengineering/rank": "^0.6.4"
}
}
+5 -1
View File
@@ -14,7 +14,7 @@
//
import activity from '@hcengineering/activity'
import type { Class, CollaborativeDoc, CollectionSize, Domain, Role, RolesAssignment } from '@hcengineering/core'
import type { Class, CollaborativeDoc, CollectionSize, Domain, Rank, Role, RolesAssignment } from '@hcengineering/core'
import { IndexKind, Account, Ref, AccountRole } from '@hcengineering/core'
import {
type Document,
@@ -130,6 +130,10 @@ export class TDocument extends TAttachedDoc implements Document, Todoable {
@Prop(Collection(time.class.ToDo), getEmbeddedLabel('Action Items'))
todos?: CollectionSize<ToDo>
@Index(IndexKind.Indexed)
@Hidden()
rank!: Rank
}
@Model(document.class.DocumentSnapshot, core.class.AttachedDoc, DOMAIN_DOCUMENT)
+34 -3
View File
@@ -13,16 +13,19 @@
// limitations under the License.
//
import { DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core'
import { DOMAIN_TX, MeasureMetricsContext, SortingOrder } from '@hcengineering/core'
import { type Document, type Teamspace } from '@hcengineering/document'
import {
tryMigrate,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient
type MigrationUpgradeClient,
type MigrateUpdate,
type MigrationDocumentQuery,
tryMigrate
} from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import { type Asset } from '@hcengineering/platform'
import { makeRank } from '@hcengineering/rank'
import document, { documentId, DOMAIN_DOCUMENT } from './index'
import { loadCollaborativeDoc, saveCollaborativeDoc, yDocCopyXmlField } from '@hcengineering/collaboration'
@@ -127,6 +130,30 @@ async function migrateContentField (client: MigrationClient): Promise<void> {
}
}
async function migrateRank (client: MigrationClient): Promise<void> {
const documents = await client.find<Document>(
DOMAIN_DOCUMENT,
{
_class: document.class.Document,
rank: { $exists: false }
},
{ sort: { name: SortingOrder.Ascending } }
)
let rank = makeRank(undefined, undefined)
const operations: { filter: MigrationDocumentQuery<Document>, update: MigrateUpdate<Document> }[] = []
for (const doc of documents) {
operations.push({
filter: { _id: doc._id },
update: { $set: { rank } }
})
rank = makeRank(rank, undefined)
}
await client.bulk(DOMAIN_DOCUMENT, operations)
}
export const documentOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, documentId, [
@@ -145,6 +172,10 @@ export const documentOperation: MigrateOperation = {
{
state: 'migrateContentField',
func: migrateContentField
},
{
state: 'migrateRank',
func: migrateRank
}
])
},
@@ -41,5 +41,9 @@
},
"dependencies": {
"@hcengineering/core": "^0.6.32"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
@@ -55,6 +55,7 @@
export let showMenu: boolean = false
export let shouldTooltip: boolean = false
export let forciblyСollapsed: boolean = false
export let draggable: boolean = false
export let actions: Action[] = []
export let _id: Ref<Doc> | string | undefined = undefined
@@ -97,6 +98,10 @@
class:selected
class:showMenu={showMenu || pressed}
on:click={toggle}
{draggable}
on:dragstart
on:dragover
on:drop
>
{#if isFold && !empty}
<button class="hulyNavGroup-header__chevron" class:collapsed={!isOpen}>
+7 -1
View File
@@ -18,7 +18,6 @@
import {
Icon,
Label,
IconOpenedArrow,
IconDown,
AnySvelteComponent,
IconSize,
@@ -56,6 +55,8 @@
export let level: number = 0
export let _id: any = undefined
export let draggable: boolean = false
let labelEl: HTMLSpanElement
let labelWidth: number
let levelReset: boolean = false
@@ -85,7 +86,12 @@
class:indent
class:disabled
class:showMenu
{draggable}
class:noActions={$$slots.actions === undefined}
on:dragstart
on:dragover
on:dragend
on:drop
on:mouseover={mouseOver}
on:mouseleave={() => {
if (levelReset && !showMenu) levelReset = false
@@ -40,6 +40,6 @@
kind="tertiary"
pressed={opened}
{dataId}
tooltip={{ label }}
tooltip={{ label, direction: 'bottom' }}
on:click={onClick}
/>
+4
View File
@@ -43,5 +43,9 @@
"@hcengineering/preference": "^0.6.13",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
+1 -1
View File
@@ -81,7 +81,7 @@
"Reacted": "Отреагировал(а)",
"Docs": "Documents",
"NewestFirst": "Сначала новые",
"ReplyToThread": "Ответить в канале",
"ReplyToThread": "Ответить в теме",
"SentMessage": "Отправил(а) сообщение",
"Direct": "Личные сообщения",
"RepliedToThread": "Ответил(а) в канале",
@@ -379,7 +379,7 @@
function messageInView (msgElement: Element, containerRect: DOMRect): boolean {
const messageRect = msgElement.getBoundingClientRect()
return messageRect.top >= containerRect.top && messageRect.bottom - messageRect.height / 2 <= containerRect.bottom
return messageRect.top >= containerRect.top && messageRect.top <= containerRect.bottom && messageRect.bottom >= 0
}
const messagesToReadAccumulator: Set<DisplayActivityMessage> = new Set<DisplayActivityMessage>()
+1 -1
View File
@@ -151,7 +151,7 @@ export async function buildThreadLink (
loc.path[2] = chunterId
}
loc.query = { message: '' }
loc.query = { ...loc.query, message: '' }
loc.path[3] = objectURI
loc.path[4] = threadParent
loc.fragment = undefined
@@ -52,7 +52,7 @@
(doc.major === $controlledDocument.major && doc.minor <= $controlledDocument.minor)
)
})
.toSorted(documentCompareFn)
.sort(documentCompareFn)
function getDescription (cc: ChangeControl | undefined): string {
if (cc === undefined) {
@@ -46,12 +46,8 @@
const id: Ref<Document> = generateId()
const object: Omit<AttachedData<Document>, 'content'> = {
name: '',
attachments: 0,
labels: 0,
comments: 0,
references: 0
const object: Pick<AttachedData<Document>, 'name' | 'icon' | 'color'> = {
name: ''
}
const dispatch = createEventDispatcher()
@@ -21,6 +21,7 @@
import { createEventDispatcher } from 'svelte'
import document from '../plugin'
import TeamspacePresenter from './teamspace/TeamspacePresenter.svelte'
import { moveDocument } from '../utils'
export let value: Document
@@ -40,10 +41,7 @@
async function save (): Promise<void> {
const ops = client.apply(value._id)
await ops.update(value, {
space,
attachedTo: parent ?? document.ids.NoParent
})
await moveDocument(value, space, parent ?? document.ids.NoParent)
if (space !== value.space) {
const children = await findChildren(value)
@@ -25,6 +25,7 @@
import document from '../../plugin'
import { createEmptyDocument } from '../../utils'
import DropArea from './DropArea.svelte'
import DocTreeElement from './DocTreeElement.svelte'
export let documents: Ref<Document>[]
@@ -34,11 +35,19 @@
export let selected: Ref<Document> | undefined
export let level: number = 0
export let onDragStart: (e: DragEvent, object: Ref<Document>) => void
export let onDragOver: (e: DragEvent, object: Ref<Document>) => void
export let onDragEnd: (e: DragEvent, object: Ref<Document>) => void
export let onDrop: (e: DragEvent, object: Ref<Document>) => void
export let draggedItem: Ref<Document> | undefined
export let draggedOver: Ref<Document> | undefined
const client = getClient()
const dispatch = createEventDispatcher()
function getDescendants (obj: Ref<Document>): Ref<Document>[] {
return (descendants.get(obj) ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((p) => p._id)
return (descendants.get(obj) ?? []).sort((a, b) => a.rank.localeCompare(b.rank)).map((p) => p._id)
}
function getActions (doc: Document): Action[] {
@@ -84,32 +93,63 @@
</script>
{#each _documents as doc}
{@const desc = _descendants.get(doc._id) ?? []}
{#if doc}
<DocTreeElement
{doc}
icon={doc.icon === view.ids.IconWithEmoji ? IconWithEmoji : doc.icon ?? document.icon.Document}
iconProps={doc.icon === view.ids.IconWithEmoji
? { icon: doc.color }
: {
fill: doc.color !== undefined ? getPlatformColorDef(doc.color, $themeStore.dark).icon : 'currentColor'
}}
title={doc.name}
selected={selected === doc._id}
isFold
{level}
empty={desc.length === 0}
actions={getActions(doc)}
moreActions={() => getMoreActions(doc)}
shouldTooltip
on:click={() => {
handleDocumentSelected(doc._id)
}}
>
{#if desc.length}
<svelte:self documents={desc} {descendants} {documentById} {selected} level={level + 1} on:selected />
{@const desc = _descendants.get(doc._id) ?? []}
{@const isDraggedOver = draggedOver === doc._id}
<div class="flex-col relative">
{#if isDraggedOver}
<DropArea />
{/if}
</DocTreeElement>
<DocTreeElement
{doc}
icon={doc.icon === view.ids.IconWithEmoji ? IconWithEmoji : doc.icon ?? document.icon.Document}
iconProps={doc.icon === view.ids.IconWithEmoji
? { icon: doc.color }
: {
fill: doc.color !== undefined ? getPlatformColorDef(doc.color, $themeStore.dark).icon : 'currentColor'
}}
title={doc.name}
selected={selected === doc._id && draggedItem === undefined}
isFold
{level}
empty={desc.length === 0}
actions={getActions(doc)}
moreActions={() => getMoreActions(doc)}
shouldTooltip
on:click={() => {
handleDocumentSelected(doc._id)
}}
on:dragstart={(evt) => {
onDragStart(evt, doc._id)
}}
on:dragover={(evt) => {
onDragOver(evt, doc._id)
}}
on:dragend={(evt) => {
onDragEnd(evt, doc._id)
}}
on:drop={(evt) => {
onDrop(evt, doc._id)
}}
>
{#if desc.length}
<svelte:self
documents={desc}
{descendants}
{documentById}
{selected}
level={level + 1}
{onDragStart}
{onDragOver}
{onDragEnd}
{onDrop}
{draggedItem}
{draggedOver}
on:selected
/>
{/if}
</DocTreeElement>
</div>
{/if}
{/each}
@@ -64,6 +64,11 @@
showMenu={hovered}
{shouldTooltip}
{forciblyСollapsed}
draggable
on:dragstart
on:dragover
on:dragend
on:drop
on:click={() => {
selectDocument()
dispatch('click')
@@ -95,4 +100,5 @@
<svelte:fragment slot="dropbox">
<slot />
</svelte:fragment>
<slot name="extra" />
</NavItem>
@@ -0,0 +1,29 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<div class="drop-area" />
<style lang="scss">
.drop-area {
pointer-events: none;
position: absolute;
left: 0.75rem;
right: 0.75rem;
top: 0;
bottom: 0;
background-color: var(--global-ui-highlight-BackgroundColor);
border-radius: 0.5rem;
}
</style>
@@ -0,0 +1,33 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let top: number
</script>
<div class="drop-marker" style="top: {top}px;" />
<style lang="scss">
.drop-marker {
pointer-events: none;
position: absolute;
z-index: 100;
height: 0.125rem;
background-color: var(--primary-button-focused);
left: 0.75rem;
right: 0.75rem;
top: 10rem;
}
</style>
@@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { Ref, SortingOrder, Space, generateId } from '@hcengineering/core'
import { Document, DocumentEvents, Teamspace } from '@hcengineering/document'
import { createQuery, getClient } from '@hcengineering/presentation'
@@ -23,7 +24,8 @@
getPlatformColorForTextDef,
themeStore,
Action,
IconAdd
IconAdd,
closeTooltip
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { TreeNode, openDoc, getActions as getContributedActions } from '@hcengineering/view-resources'
@@ -31,10 +33,17 @@
import { getResource } from '@hcengineering/platform'
import document from '../../plugin'
import { getDocumentIdFromFragment, createEmptyDocument } from '../../utils'
import {
getDocumentIdFromFragment,
createEmptyDocument,
moveDocument,
moveDocumentBefore,
moveDocumentAfter
} from '../../utils'
import DocHierarchy from './DocHierarchy.svelte'
import DocTreeElement from './DocTreeElement.svelte'
import { Analytics } from '@hcengineering/analytics'
import DropArea from './DropArea.svelte'
import DropMarker from './DropMarker.svelte'
export let space: Teamspace
export let model: SpacesNavModel
@@ -52,7 +61,24 @@
let descendants: Map<Ref<Document>, Document[]> = new Map<Ref<Document>, Document[]>()
function getDescendants (obj: Ref<Document>): Ref<Document>[] {
return (descendants.get(obj) ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((p) => p._id)
return (descendants.get(obj) ?? []).sort((a, b) => a.rank.localeCompare(b.rank)).map((p) => p._id)
}
function getAllDescendants (obj: Ref<Document>): Ref<Document>[] {
const result: Ref<Document>[] = []
const queue: Ref<Document>[] = [obj]
while (queue.length > 0) {
const next = queue.pop()
if (next === undefined) break
const children = descendants.get(next) ?? []
const childrenRefs = children.map((p) => p._id)
result.push(...childrenRefs)
queue.push(...childrenRefs)
}
return result
}
let selected: Ref<Document> | undefined
@@ -85,7 +111,7 @@
},
{
sort: {
name: SortingOrder.Ascending
rank: SortingOrder.Ascending
}
}
)
@@ -125,48 +151,180 @@
return result
}
let parent: HTMLElement
let draggedItem: Ref<Document> | undefined = undefined
let draggedOver: Ref<Document> | undefined = undefined
let draggedOverPos: 'before' | 'after' | undefined = undefined
let draggedOverTop: number = 0
let cannotDropTo: Ref<Document>[] = []
function canDrop (object: Ref<Document>, target: Ref<Document>): boolean {
if (object === target) return false
if (cannotDropTo.includes(target)) return false
return true
}
function onDragStart (event: DragEvent, object: Ref<Document>): void {
// no prevent default to leverage default rendering
// event.preventDefault()
if (event.dataTransfer === null || event.target === null) {
return
}
cannotDropTo = [object, ...getAllDescendants(object)]
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.dropEffect = 'move'
draggedItem = object
closeTooltip()
}
function getDropPosition (event: DragEvent): { pos: 'before' | 'after' | undefined, top: number } {
const parentRect = parent.getBoundingClientRect()
const targetRect = (event.target as HTMLElement).getBoundingClientRect()
const dropPosition = event.clientY - targetRect.top
const before = dropPosition >= 0 && dropPosition < targetRect.height / 6
const after = dropPosition <= targetRect.height && dropPosition > (5 * targetRect.height) / 6
const pos = before ? 'before' : after ? 'after' : undefined
const top = pos === 'before' ? targetRect.top - parentRect.top - 1 : targetRect.bottom - parentRect.top - 1
return { pos, top }
}
function onDragOver (event: DragEvent, object: Ref<Document>): void {
event.preventDefault()
// this is an ugly solution to control drop effect
// we drag and drop elements that are in the depth of components hierarchy
// so we cannot access them directly
if (!(event.target as HTMLElement).draggable) return
if (event.dataTransfer === null || event.target === null || draggedItem === object) {
return
}
if (draggedItem !== undefined && canDrop(draggedItem, object)) {
event.dataTransfer.dropEffect = 'move'
draggedOver = object
const { pos, top } = getDropPosition(event)
draggedOverPos = pos
draggedOverTop = top
} else {
event.dataTransfer.dropEffect = 'none'
}
}
function onDragEnd (event: DragEvent): void {
event.preventDefault()
draggedItem = undefined
draggedOver = undefined
draggedOverPos = undefined
}
function onDrop (event: DragEvent, object: Ref<Document>): void {
event.preventDefault()
if (event.dataTransfer === null) {
return
}
if (draggedItem !== undefined && canDrop(draggedItem, object)) {
const doc = documentById.get(draggedItem)
const target = documentById.get(object)
if (doc !== undefined && doc._id !== object) {
if (object === document.ids.NoParent) {
void moveDocument(doc, doc.space, document.ids.NoParent)
} else if (target !== undefined) {
const { pos } = getDropPosition(event)
if (pos === 'before') {
void moveDocumentBefore(doc, target)
} else if (pos === 'after') {
void moveDocumentAfter(doc, target)
} else if (doc.attachedTo !== object) {
void moveDocument(doc, target.space, target._id)
}
}
}
}
draggedItem = undefined
draggedOver = undefined
}
</script>
<TreeNode
_id={space?._id}
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
iconProps={space?.icon === view.ids.IconWithEmoji
? { icon: space.color }
: {
fill:
space.color !== undefined
? getPlatformColorDef(space.color, $themeStore.dark).icon
: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
}}
title={space.name}
type={'nested'}
highlighted={currentSpace === space._id}
visible={currentSpace === space._id || forciblyСollapsed}
actions={() => getActions(space)}
{forciblyСollapsed}
>
<DocHierarchy {documents} {descendants} {documentById} {selected} />
<div bind:this={parent} class="flex-col relative">
{#if draggedOver === document.ids.NoParent}
<DropArea />
{/if}
<svelte:fragment slot="visible">
{#if (selected || forciblyСollapsed) && visibleItem}
{@const item = visibleItem}
<DocTreeElement
doc={item}
icon={item.icon === view.ids.IconWithEmoji ? IconWithEmoji : item.icon ?? document.icon.Document}
iconProps={item.icon === view.ids.IconWithEmoji
? { icon: visibleItem.color }
: {
fill: item.color !== undefined ? getPlatformColorDef(item.color, $themeStore.dark).icon : 'currentColor'
}}
title={item.name}
selected
isFold
empty
shouldTooltip
actions={getDocActions(item)}
moreActions={() => getMoreActions(item)}
forciblyСollapsed
/>
{/if}
</svelte:fragment>
</TreeNode>
{#if draggedOver && draggedOverPos}
<DropMarker top={draggedOverTop} />
{/if}
<TreeNode
_id={space?._id}
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
iconProps={space?.icon === view.ids.IconWithEmoji
? { icon: space.color }
: {
fill:
space.color !== undefined
? getPlatformColorDef(space.color, $themeStore.dark).icon
: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
}}
title={space.name}
type={'nested'}
highlighted={currentSpace === space._id}
visible={currentSpace === space._id || forciblyСollapsed}
actions={() => getActions(space)}
selected={draggedOver === document.ids.NoParent}
{forciblyСollapsed}
draggable
on:drop={(evt) => {
onDrop(evt, document.ids.NoParent)
}}
on:dragover={(evt) => {
onDragOver(evt, document.ids.NoParent)
}}
on:dragstart={(evt) => {
evt.preventDefault()
}}
>
<DocHierarchy
{documents}
{descendants}
{documentById}
{selected}
{onDragStart}
{onDragEnd}
{onDragOver}
{onDrop}
{draggedItem}
{draggedOver}
/>
<svelte:fragment slot="visible">
{#if (selected || forciblyСollapsed) && visibleItem}
{@const item = visibleItem}
<DocTreeElement
doc={item}
icon={item.icon === view.ids.IconWithEmoji ? IconWithEmoji : item.icon ?? document.icon.Document}
iconProps={item.icon === view.ids.IconWithEmoji
? { icon: visibleItem.color }
: {
fill: item.color !== undefined ? getPlatformColorDef(item.color, $themeStore.dark).icon : 'currentColor'
}}
title={item.name}
selected
isFold
empty
shouldTooltip
actions={getDocActions(item)}
moreActions={() => getMoreActions(item)}
forciblyСollapsed
/>
{/if}
</svelte:fragment>
</TreeNode>
</div>
+47 -3
View File
@@ -13,17 +13,57 @@
// limitations under the License.
//
import { type AttachedData, type Client, type Ref, type TxOperations, makeCollaborativeDoc } from '@hcengineering/core'
import { type Document, type Teamspace, documentId } from '@hcengineering/document'
import {
type AttachedData,
type Client,
type QuerySelector,
type Ref,
SortingOrder,
type TxOperations,
makeCollaborativeDoc
} from '@hcengineering/core'
import { type Document, type Teamspace, documentId, getFirstRank } from '@hcengineering/document'
import { getMetadata, translate } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import { makeRank } from '@hcengineering/rank'
import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui'
import { accessDeniedStore } from '@hcengineering/view-resources'
import { workbenchId } from '@hcengineering/workbench'
import slugify from 'slugify'
import { accessDeniedStore } from '@hcengineering/view-resources'
import document from './plugin'
export async function moveDocument (doc: Document, space: Ref<Teamspace>, parent: Ref<Document>): Promise<void> {
const client = getClient()
const prevRank = await getFirstRank(client, space, parent)
const rank = makeRank(prevRank, undefined)
await client.update(doc, { space, attachedTo: parent, rank })
}
export async function moveDocumentBefore (doc: Document, before: Document): Promise<void> {
const client = getClient()
const { space, attachedTo } = before
const query = { rank: { $lt: before.rank } as unknown as QuerySelector<Document['rank']> }
const lastRank = await getFirstRank(client, space, attachedTo, SortingOrder.Descending, query)
const rank = makeRank(lastRank, before.rank)
await client.update(doc, { space, attachedTo, rank })
}
export async function moveDocumentAfter (doc: Document, after: Document): Promise<void> {
const client = getClient()
const { space, attachedTo } = after
const query = { rank: { $gt: after.rank } as unknown as QuerySelector<Document['rank']> }
const nextRank = await getFirstRank(client, space, attachedTo, SortingOrder.Ascending, query)
const rank = makeRank(after.rank, nextRank)
await client.update(doc, { space, attachedTo, rank })
}
export async function createEmptyDocument (
client: TxOperations,
id: Ref<Document>,
@@ -33,6 +73,9 @@ export async function createEmptyDocument (
): Promise<void> {
const name = await translate(document.string.Untitled, {})
const lastRank = await getFirstRank(client, space, parent)
const rank = makeRank(lastRank, undefined)
const object: AttachedData<Document> = {
name,
content: makeCollaborativeDoc(id, 'content'),
@@ -42,6 +85,7 @@ export async function createEmptyDocument (
labels: 0,
comments: 0,
references: 0,
rank,
...data
}
+2 -1
View File
@@ -15,8 +15,9 @@
import { documentId, documentPlugin } from './plugin'
export * from './types'
export * from './analytics'
export * from './types'
export * from './utils'
export { documentId }
export default documentPlugin
+3 -1
View File
@@ -14,7 +14,7 @@
//
import { Attachment } from '@hcengineering/attachment'
import { Account, AttachedDoc, Class, CollaborativeDoc, Ref, TypedSpace } from '@hcengineering/core'
import { Account, AttachedDoc, Class, CollaborativeDoc, Rank, Ref, TypedSpace } from '@hcengineering/core'
import { Preference } from '@hcengineering/preference'
import { IconProps } from '@hcengineering/view'
@@ -37,6 +37,8 @@ export interface Document extends AttachedDoc<Document, 'children', Teamspace>,
embeddings?: number
labels?: number
references?: number
rank: Rank
}
/** @public */
+35
View File
@@ -0,0 +1,35 @@
//
// Copyright © 2023, 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type DocumentQuery, type Rank, type Ref, SortingOrder, TxOperations } from '@hcengineering/core'
import document from './plugin'
import { type Document, type Teamspace } from './types'
export async function getFirstRank (
client: TxOperations,
space: Ref<Teamspace>,
attachedTo: Ref<Document>,
sort: SortingOrder = SortingOrder.Descending,
extra: DocumentQuery<Document> = {}
): Promise<Rank | undefined> {
const doc = await client.findOne(
document.class.Document,
{ space, attachedTo, ...extra },
{ sort: { rank: sort }, projection: { rank: 1 } }
)
return doc?.rank
}
@@ -15,7 +15,7 @@
<script lang="ts">
import activity, { ActivityMessage } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
import { Class, Doc, getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
import { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import {
@@ -68,6 +68,9 @@
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
let urlObjectId: Ref<Doc> | undefined = undefined
let urlObjectClass: Ref<Class<Doc>> | undefined = undefined
let showArchive = false
let archivedActivityNotifications: InboxNotification[] = []
let archivedOtherNotifications: InboxNotification[] = []
@@ -165,14 +168,19 @@
if (loc?.loc.path[3] == null) {
selectedContext = undefined
urlObjectId = undefined
urlObjectClass = undefined
restoreLocation(newLocation, notificationId)
return
}
const [id, _class] = decodeObjectURI(loc?.loc.path[3] ?? '')
const _id = await parseLinkId(linkProviders, id, _class)
urlObjectId = _id
urlObjectClass = _class
const thread = loc?.loc.path[4] as Ref<ActivityMessage>
const context = $contextByDocStore.get(thread) ?? $contextByDocStore.get(_id)
const queryContext = loc.loc.query?.context as Ref<DocNotifyContext>
const context = $contextByIdStore.get(queryContext) ?? $contextByDocStore.get(thread) ?? $contextByDocStore.get(_id)
selectedContextId = context?._id
@@ -199,7 +207,7 @@
$: selectedContext = selectedContextId ? selectedContext ?? $contextByIdStore.get(selectedContextId) : undefined
$: void updateSelectedPanel(selectedContext)
$: void updateSelectedPanel(selectedContext, urlObjectClass)
$: void updateTabItems(inboxData, $contextsStore)
async function updateTabItems (inboxData: InboxData, notifyContexts: DocNotifyContext[]): Promise<void> {
@@ -258,15 +266,28 @@
void selectInboxContext(linkProviders, selectedContext, selectedNotification, event?.detail.object)
}
function isChunterChannel (selectedContext: DocNotifyContext, urlObjectClass?: Ref<Class<Doc>>): boolean {
const isActivityMessageContext = hierarchy.isDerived(selectedContext.objectClass, activity.class.ActivityMessage)
const chunterClass = isActivityMessageContext
? urlObjectClass ?? selectedContext.objectClass
: selectedContext.objectClass
return hierarchy.isDerived(chunterClass, chunter.class.ChunterSpace)
}
async function updateSelectedPanel (selectedContext?: DocNotifyContext): Promise<void> {
async function updateSelectedPanel (
selectedContext?: DocNotifyContext,
urlObjectClass?: Ref<Class<Doc>>
): Promise<void> {
if (selectedContext === undefined) {
selectedComponent = undefined
return
}
const isChunterChannel = hierarchy.isDerived(selectedContext.objectClass, chunter.class.ChunterSpace)
const panelComponent = hierarchy.classHierarchyMixin(selectedContext.objectClass, view.mixin.ObjectPanel)
const isChunter = isChunterChannel(selectedContext, urlObjectClass)
const panelComponent = hierarchy.classHierarchyMixin(
isChunter ? urlObjectClass ?? selectedContext.objectClass : selectedContext.objectClass,
view.mixin.ObjectPanel
)
selectedComponent = panelComponent?.component ?? view.component.EditDoc
@@ -278,7 +299,7 @@
ops,
contextNotifications
.filter(({ _class, isViewed }) =>
isChunterChannel ? _class === notification.class.CommonInboxNotification : !isViewed
isChunter ? _class === notification.class.CommonInboxNotification : !isViewed
)
.map(({ _id }) => _id)
)
@@ -432,8 +453,12 @@
<Component
is={selectedComponent}
props={{
_id: selectedContext.objectId,
_class: selectedContext.objectClass,
_id: isChunterChannel(selectedContext, urlObjectClass)
? urlObjectId ?? selectedContext.objectId
: selectedContext.objectId,
_class: isChunterChannel(selectedContext, urlObjectClass)
? urlObjectClass ?? selectedContext.objectClass
: selectedContext.objectClass,
context: selectedContext,
activityMessage: selectedMessage,
props: { context: selectedContext }
+7 -1
View File
@@ -531,6 +531,7 @@ async function generateLocation (
async function navigateToInboxDoc (
providers: LinkIdProvider[],
context: Ref<DocNotifyContext>,
_id?: Ref<Doc>,
_class?: Ref<Class<Doc>>,
thread?: Ref<ActivityMessage>,
@@ -559,7 +560,7 @@ async function navigateToInboxDoc (
loc.path.length = 4
}
loc.query = { ...loc.query, message: message ?? null }
loc.query = { ...loc.query, context, message: message ?? null }
messageInFocus.set(message)
Analytics.handleEvent('inbox.ReadDoc', { objectId: id, objectClass: _class, thread, message })
navigate(loc)
@@ -596,6 +597,7 @@ export async function selectInboxContext (
void navigateToInboxDoc(
linkProviders,
context._id,
objectId,
objectClass,
isActivityMessageClass(objectClass) ? (objectId as Ref<ActivityMessage>) : undefined,
@@ -621,6 +623,7 @@ export async function selectInboxContext (
void navigateToInboxDoc(
linkProviders,
context._id,
thread?.objectId ?? objectId,
thread?.objectClass ?? objectClass,
thread?.attachedTo,
@@ -642,6 +645,7 @@ export async function selectInboxContext (
void navigateToInboxDoc(
linkProviders,
context._id,
channelId,
channelClass,
thread as Ref<ActivityMessage>,
@@ -657,6 +661,7 @@ export async function selectInboxContext (
void navigateToInboxDoc(
linkProviders,
context._id,
channelId,
channelClass,
thread as Ref<ActivityMessage>,
@@ -667,6 +672,7 @@ export async function selectInboxContext (
void navigateToInboxDoc(
linkProviders,
context._id,
objectId,
objectClass,
undefined,
+1
View File
@@ -7,6 +7,7 @@
"build": "compile ui",
"build:watch": "compile ui",
"format": "format src",
"svelte-check": "do-svelte-check",
"_phase:build": "compile ui",
"_phase:format": "format src",
"_phase:validate": "compile validate"
@@ -45,12 +45,12 @@
$: if (showDiff && assessmentData !== null) {
indices = assessmentData.correctOrder
.map((position, index) => [position, index])
.toSorted(([aPosition], [bPosition]) => (aPosition > bPosition ? 1 : aPosition < bPosition ? -1 : 0))
.sort(([aPosition], [bPosition]) => (aPosition > bPosition ? 1 : aPosition < bPosition ? -1 : 0))
.map(([_, index]) => index) as [number, ...number[]]
} else if (answerData !== null) {
indices = answerData.order
.map((position, index) => [position, index])
.toSorted(([aPosition], [bPosition]) => (aPosition > bPosition ? 1 : aPosition < bPosition ? -1 : 0))
.sort(([aPosition], [bPosition]) => (aPosition > bPosition ? 1 : aPosition < bPosition ? -1 : 0))
.map(([_, index]) => index) as [number, ...number[]]
} else {
indices = questionData.options.map((_, index) => index) as [number, ...number[]]
@@ -67,7 +67,7 @@
indices = moveItem(indices, from, to)
const order = indices
.map((initialIndex, index) => [initialIndex, index])
.toSorted(([aInitialIndex], [bInitialIndex]) =>
.sort(([aInitialIndex], [bInitialIndex]) =>
aInitialIndex > bInitialIndex ? 1 : aInitialIndex < bInitialIndex ? -1 : 0
)
.map(([_, index]) => index + 1) as [OrderingPosition, ...OrderingPosition[]]
@@ -16,6 +16,8 @@ MultipleChoiceAssessmentAnswer
return {
score:
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
answerData.selectedIndices.toSorted().join('~') === assessmentData.correctIndices.toSorted().join('~') ? 100 : 0
answerData.selectedIndices.slice().sort().join('~') === assessmentData.correctIndices.slice().sort().join('~')
? 100
: 0
}
}
@@ -15,7 +15,7 @@
-->
<script lang="ts">
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import core, { ClassifierKind, Data, Doc, Mixin, Ref } from '@hcengineering/core'
import core, { ClassifierKind, type CollaborativeDoc, Data, Doc, Mixin, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
@@ -35,7 +35,7 @@
let object: Required<Vacancy>
let rawName: string = ''
let rawDesc: string = ''
let rawFullDesc: string = ''
let rawFullDesc: CollaborativeDoc
let lastId: Ref<Vacancy> | undefined = undefined
let showAllMixins = false
@@ -26,10 +26,14 @@
let candidate: Candidate | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
$: spaceQuery.query(recruit.class.Vacancy, { _id: value.space }, (res) => ([currentVacancy] = res))
$: spaceQuery.query(recruit.class.Vacancy, { _id: value.space }, (res) => {
;[currentVacancy] = res
})
const shortLabel = value && hierarchy.getClass(value._class).shortLabel
$: candidateQuery.query(recruit.mixin.Candidate, { _id: value.attachedTo }, (res) => ([candidate] = res))
$: candidateQuery.query(recruit.mixin.Candidate, { _id: value.attachedTo }, (res) => {
;[candidate] = res
})
$: title = `${shortLabel}-${value?.number}`
</script>
@@ -232,7 +232,8 @@
goodTagMap = toIdMap(goodTags)
const goodSortedTags = goodTags
.toSorted((a, b) => b.title.length - a.title.length)
.slice()
.sort((a, b) => b.title.length - a.title.length)
.filter((t) => t.title.length > 2)
const goodSortedTagsTitles = new Map<Ref<TagElement>, string>()
processed = -1
@@ -251,7 +252,7 @@
const tagElementIds = new Map<Ref<TagElement>, TagUpdatePlan['elements'][0]>()
for (const tag of tagElements.toSorted((a, b) => prepareTitle(a.title).length - prepareTitle(b.title).length)) {
for (const tag of tagElements.slice().sort((a, b) => prepareTitle(a.title).length - prepareTitle(b.title).length)) {
processed++
const refs = allRefs.filter((it) => it.tag === tag._id)
if (goodTagMap.has(tag._id)) {
@@ -14,7 +14,9 @@
// limitations under the License.
-->
<script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large'
import { IconSize } from '@hcengineering/ui'
export let size: IconSize
const fill: string = 'currentColor'
</script>
@@ -143,14 +143,14 @@
...gitem.events,
...gitem.busyEvents,
...gitem.busy.slots
].toSorted((a, b) => a.date - b.date)}
].sort((a, b) => a.date - b.date)}
<div style:overflow-x={'hidden'} style:overflow-y={'auto'} style:height="{height}rem">
<div class="flex flex-row-center">
<div class="flex-nowrap p-1 w-full" style:display={'inline-flex'}>
{#each Array.from(Array(24).keys()) as hour}
{@const _slots = slots
.filter((it) => new Date(it.date).getHours() === hour)
.toSorted((a, b) => a.date - b.date)}
.sort((a, b) => a.date - b.date)}
{@const cwidth = hourWidths[hour]}
<div class="flex-col" style:width="{cwidth}rem">
{#each _slots as m, i}
+4
View File
@@ -44,5 +44,9 @@
"@hcengineering/platform": "^0.6.11",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/rank": "^0.6.4"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
+1
View File
@@ -7,6 +7,7 @@
"build": "compile ui",
"build:watch": "compile ui",
"format": "format src",
"svelte-check": "do-svelte-check",
"_phase:build": "compile ui",
"_phase:format": "format src",
"_phase:validate": "compile validate"
@@ -43,7 +43,7 @@
_id: employee._id as Ref<Item>,
completion: completionMap.get(employee._id) as CompletionMapValue
}))
.toSorted((item1, item2) => compareCompletionMapValueState(item1.completion.state, item2.completion.state))
.sort((item1, item2) => compareCompletionMapValueState(item1.completion.state, item2.completion.state))
},
{
sort: {
@@ -9,7 +9,7 @@ export async function trainingAttemptStateSort (
_: TxOperations,
states: TrainingAttemptState[]
): Promise<TrainingAttemptState[]> {
return states.toSorted(
(state1, state2) => trainingAttemptStateOrder.indexOf(state2) - trainingAttemptStateOrder.indexOf(state1)
)
return states
.slice()
.sort((state1, state2) => trainingAttemptStateOrder.indexOf(state2) - trainingAttemptStateOrder.indexOf(state1))
}
@@ -17,5 +17,7 @@ import type { TxOperations } from '@hcengineering/core'
import { type TrainingState, trainingStateOrder } from '@hcengineering/training'
export async function trainingStateSort (_: TxOperations, states: TrainingState[]): Promise<TrainingState[]> {
return states.toSorted((state1, state2) => trainingStateOrder.indexOf(state1) - trainingStateOrder.indexOf(state2))
return states
.slice()
.sort((state1, state2) => trainingStateOrder.indexOf(state1) - trainingStateOrder.indexOf(state2))
}
@@ -55,6 +55,7 @@
export let showNotify: boolean = false
export let forciblyСollapsed: boolean = false
export let actions: (originalEvent?: MouseEvent) => Promise<Action[]> = async () => []
export let draggable: boolean = false
let pressed: boolean = false
let inlineActions: Action[] = []
@@ -103,7 +104,11 @@
{shouldTooltip}
showMenu={showMenu || pressed}
{noDivider}
{draggable}
on:click
on:dragstart
on:dragover
on:drop
on:toggle={(ev) => {
if (ev.detail !== undefined) collapsed = !ev.detail
}}
@@ -166,8 +171,12 @@
{forciblyСollapsed}
{level}
{shouldTooltip}
{draggable}
showMenu={showMenu || pressed}
on:click
on:dragstart
on:dragover
on:drop
>
<slot />
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
@@ -39,6 +39,7 @@
export let noDivider: boolean = false
export let shouldTooltip: boolean = false
export let forciblyСollapsed: boolean = false
export let draggable: boolean = false
</script>
<TreeElement
@@ -63,7 +64,11 @@
{showMenu}
{noDivider}
{forciblyСollapsed}
{draggable}
on:click
on:dragstart
on:dragover
on:drop
>
<slot />
<svelte:fragment slot="extra"><slot name="extra" /></svelte:fragment>
+1
View File
@@ -8,6 +8,7 @@
"build": "compile ui",
"build:docs": "api-extractor run --local",
"format": "format src",
"svelte-check": "do-svelte-check",
"build:watch": "compile ui",
"_phase:build": "compile ui",
"_phase:format": "format src",
@@ -98,7 +98,7 @@
{/each}
{#each Object.entries(metrics.params) as [k, v], i}
<div style:margin-left={`${level * 0.5}rem`}>
{#each Object.entries(v).toSorted((a, b) => b[1].value / (b[1].operations + 1) - a[1].value / (a[1].operations + 1)) as [kk, vv]}
{#each Object.entries(v).sort((a, b) => b[1].value / (b[1].operations + 1) - a[1].value / (a[1].operations + 1)) as [kk, vv]}
{@const childExpandable =
vv.topResult !== undefined &&
vv.topResult.length > 0 &&
+84 -4
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { ObjectId as MongoObjectId } from 'mongodb'
import type { Collection, Db, Filter, OptionalUnlessRequiredId, Sort } from 'mongodb'
import type { Collection, CreateIndexesOptions, Db, Filter, OptionalUnlessRequiredId, Sort } from 'mongodb'
import type { Data, Version } from '@hcengineering/core'
import type {
@@ -31,6 +31,12 @@ import type {
OtpRecord,
UpgradeStatistic
} from '../types'
import { isShallowEqual } from '../utils'
interface MongoIndex {
key: Record<string, any>
options: CreateIndexesOptions & { name: string }
}
export class MongoDbCollection<T extends Record<string, any>> implements DbCollection<T> {
constructor (
@@ -43,7 +49,55 @@ export class MongoDbCollection<T extends Record<string, any>> implements DbColle
}
async init (): Promise<void> {
// May be used to create indicex in Mongo
// May be used to create indices in Mongo
}
/**
* Ensures indices in the collection or creates new if needed.
* Drops all other indices that are not in the list.
* @param indicesToEnsure MongoIndex
*/
async ensureIndices (indicesToEnsure: MongoIndex[]): Promise<void> {
try {
const indices = await this.collection.listIndexes().toArray()
for (const idx of indices) {
if (idx.key._id !== undefined) {
continue
}
const isEqualIndex = (ensureIdx: MongoIndex): boolean => {
const { key, options } = ensureIdx
const sameKeys = isShallowEqual(idx.key, key)
if (!sameKeys) {
return false
}
const shortIdxOptions = { ...idx }
delete shortIdxOptions.key
delete shortIdxOptions.v
return isShallowEqual(shortIdxOptions, options)
}
if (indicesToEnsure.some(isEqualIndex)) {
continue
}
await this.collection.dropIndex(idx.name)
}
} catch (e: any) {
if (e?.codeName === 'NamespaceNotFound') {
// Nothing to do, new DB
} else {
throw e
}
}
for (const { key, options } of indicesToEnsure) {
await this.collection.createIndex(key, options)
}
}
async find (query: Query<T>, sort?: { [P in keyof T]?: 'ascending' | 'descending' }, limit?: number): Promise<T[]> {
@@ -99,7 +153,14 @@ export class AccountMongoDbCollection extends MongoDbCollection<Account> impleme
}
async init (): Promise<void> {
await this.collection.createIndex({ email: 1 }, { unique: true })
const indicesToEnsure: MongoIndex[] = [
{
key: { email: 1 },
options: { unique: true, name: 'hc_account_email_1' }
}
]
await this.ensureIndices(indicesToEnsure)
}
convertToObj (acc: Account): Account {
@@ -133,7 +194,26 @@ export class WorkspaceMongoDbCollection extends MongoDbCollection<Workspace> imp
}
async init (): Promise<void> {
await this.collection.createIndex({ workspace: 1 }, { unique: true })
// await this.collection.createIndex({ workspace: 1 }, { unique: true })
const indicesToEnsure: MongoIndex[] = [
{
key: { workspace: 1 },
options: {
unique: true,
name: 'hc_account_workspace_1'
}
},
{
key: { workspaceUrl: 1 },
options: {
unique: true,
name: 'hc_account_workspaceUrl_1'
}
}
]
await this.ensureIndices(indicesToEnsure)
}
async countWorkspacesInRegion (region: string, upToVersion?: Data<Version>, visitedSince?: number): Promise<number> {
+7
View File
@@ -187,3 +187,10 @@ export function areDbIdsEqual (obj1: any, obj2: any): boolean {
return obj1 === obj2
}
export function isShallowEqual (obj1: Record<string, any>, obj2: Record<string, any>): boolean {
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
return keys1.length === keys2.length && keys1.every((k) => obj1[k] === obj2[k])
}
+105 -6
View File
@@ -26,6 +26,14 @@ export interface ObjectMetadata {
size?: number
}
/** @public */
export interface StatObjectOutput {
lastModified: number
type: string
etag?: string
size?: number
}
/** @public */
export interface PutObjectOutput {
id: string
@@ -55,9 +63,19 @@ export class Client {
async getObject (ctx: MeasureContext, workspace: WorkspaceId, objectName: string): Promise<Readable> {
const url = this.getObjectUrl(ctx, workspace, objectName)
const response = await fetch(url)
let response
try {
response = await fetch(url)
} catch (err: any) {
ctx.error('network error', { error: err })
throw new Error(`Network error ${err}`)
}
if (!response.ok) {
if (response.status === 404) {
throw new Error('Not Found')
}
throw new Error('HTTP error ' + response.status)
}
@@ -69,12 +87,90 @@ export class Client {
return Readable.from(response.body)
}
async getPartialObject (
ctx: MeasureContext,
workspace: WorkspaceId,
objectName: string,
offset: number,
length?: number
): Promise<Readable> {
const url = this.getObjectUrl(ctx, workspace, objectName)
const headers = {
Range: `bytes=${offset}-${length ?? ''}`
}
let response
try {
response = await fetch(url, { headers })
} catch (err: any) {
ctx.error('network error', { error: err })
throw new Error(`Network error ${err}`)
}
if (!response.ok) {
if (response.status === 404) {
throw new Error('Not Found')
}
throw new Error('HTTP error ' + response.status)
}
if (response.body == null) {
ctx.error('bad datalake response', { objectName })
throw new Error('Missing response body')
}
return Readable.from(response.body)
}
async statObject (
ctx: MeasureContext,
workspace: WorkspaceId,
objectName: string
): Promise<StatObjectOutput | undefined> {
const url = this.getObjectUrl(ctx, workspace, objectName)
let response
try {
response = await fetch(url, { method: 'HEAD' })
} catch (err: any) {
ctx.error('network error', { error: err })
throw new Error(`Network error ${err}`)
}
if (!response.ok) {
if (response.status === 404) {
return undefined
}
throw new Error('HTTP error ' + response.status)
}
const headers = response.headers
const lastModified = Date.parse(headers.get('Last-Modified') ?? '')
const size = parseInt(headers.get('Content-Length') ?? '0', 10)
return {
lastModified: isNaN(lastModified) ? 0 : lastModified,
size: isNaN(size) ? 0 : size,
type: headers.get('Content-Type') ?? '',
etag: headers.get('ETag') ?? ''
}
}
async deleteObject (ctx: MeasureContext, workspace: WorkspaceId, objectName: string): Promise<void> {
const url = this.getObjectUrl(ctx, workspace, objectName)
const response = await fetch(url, { method: 'DELETE' })
let response
try {
response = await fetch(url, { method: 'DELETE' })
} catch (err: any) {
ctx.error('network error', { error: err })
throw new Error(`Network error ${err}`)
}
if (!response.ok) {
if (response.status === 404) {
throw new Error('Not Found')
}
throw new Error('HTTP error ' + response.status)
}
}
@@ -100,10 +196,13 @@ export class Client {
}
form.append('file', stream, options)
const response = await fetch(url, {
method: 'POST',
body: form
})
let response
try {
response = await fetch(url, { method: 'POST', body: form })
} catch (err: any) {
ctx.error('network error', { error: err })
throw new Error(`Network error ${err}`)
}
if (!response.ok) {
throw new Error('HTTP error ' + response.status)
+28 -5
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { withContext, type Blob, type MeasureContext, type WorkspaceId } from '@hcengineering/core'
import core, { type Blob, type MeasureContext, type Ref, type WorkspaceId, withContext } from '@hcengineering/core'
import {
type BlobStorageIterator,
@@ -74,13 +74,36 @@ export class DatalakeService implements StorageAdapter {
@withContext('listStream')
async listStream (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<BlobStorageIterator> {
throw new Error('not supported')
return {
next: async () => [],
close: async () => {}
}
}
@withContext('stat')
async stat (ctx: MeasureContext, workspaceId: WorkspaceId, objectName: string): Promise<Blob | undefined> {
// not supported
return undefined
try {
const result = await this.client.statObject(ctx, workspaceId, objectName)
if (result !== undefined) {
return {
provider: '',
_class: core.class.Blob,
_id: objectName as Ref<Blob>,
storageId: objectName,
contentType: result.type,
size: result.size ?? 0,
etag: result.etag ?? '',
space: core.space.Configuration,
modifiedBy: core.account.System,
modifiedOn: result.lastModified,
version: null
}
} else {
ctx.error('no object found', { objectName, workspaceId: workspaceId.name })
}
} catch (err) {
ctx.error('failed to stat object', { error: err, objectName, workspaceId: workspaceId.name })
}
}
@withContext('get')
@@ -134,7 +157,7 @@ export class DatalakeService implements StorageAdapter {
offset: number,
length?: number
): Promise<Readable> {
throw new Error('not implemented')
return await this.client.getPartialObject(ctx, workspaceId, objectName, offset, length)
}
async getUrl (ctx: MeasureContext, workspaceId: WorkspaceId, objectName: string): Promise<string> {
@@ -700,10 +700,12 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
const approvedOrChangesRequested = new Map<Ref<Person>, PullRequestReviewState>()
const reviewStates = new Map<Ref<Person>, PullRequestReviewState[]>()
const sortedReviews: (Review & { date: number })[] = external.reviews.nodes.map((it) => ({
...it,
date: new Date(it.updatedAt ?? it.submittedAt ?? it.createdAt).getTime()
}))
const sortedReviews: (Review & { date: number })[] = external.reviews.nodes
.filter((it) => it != null)
.map((it) => ({
...it,
date: new Date(it.updatedAt ?? it.submittedAt ?? it.createdAt).getTime()
}))
for (const it of external.latestReviews.nodes) {
if (sortedReviews.some((qt) => it.id === qt.id)) {
+1 -1
View File
@@ -346,7 +346,7 @@ export async function syncDerivedDocuments<T extends { url: string }> (
})
const processed = new Set<Ref<DocSyncInfo>>()
const _docs = docs(ext)
const _docs = docs(ext).filter((it) => it != null)
for (const r of _docs) {
const existing = childDocsOfClass.find((it) => it.url.toLowerCase() === r.url.toLowerCase())
if (existing === undefined) {