Merge remote-tracking branch 'origin/develop'

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2024-08-02 21:15:47 +07:00
166 changed files with 1786 additions and 1577 deletions
+1
View File
@@ -100,6 +100,7 @@
"ACCOUNTS_URL": "http://localhost:3000",
"UPLOAD_URL": "/files",
"SERVER_PORT": "8087",
"VERSION": null,
"COLLABORATOR_URL": "ws://localhost:3078",
"COLLABORATOR_API_URL": "http://localhost:3078",
"CALENDAR_URL": "http://localhost:8095",
+1 -1
View File
@@ -1 +1 @@
"0.6.270"
"0.6.271"
+6
View File
@@ -77,6 +77,7 @@ services:
- ACCOUNTS_URL=http://account:3000
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName":"collaborator","maxPoolSize":2}'
- STORAGE_CONFIG=${STORAGE_CONFIG}
restart: unless-stopped
front:
@@ -95,6 +96,7 @@ services:
- SERVER_PORT=8080
- SERVER_SECRET=secret
- MONGO_URL=mongodb://mongodb:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName":"front","maxPoolSize":1}'
- ACCOUNTS_URL=http://localhost:3000
- UPLOAD_URL=/files
- ELASTIC_URL=http://elastic:9200
@@ -135,6 +137,7 @@ services:
- ENABLE_COMPRESSION=true
- ELASTIC_URL=http://elastic:9200
- MONGO_URL=mongodb://mongodb:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName": "transactor", "maxPoolSize": 1}'
- METRICS_CONSOLE=false
- METRICS_FILE=metrics.txt
- STORAGE_CONFIG=${STORAGE_CONFIG}
@@ -165,6 +168,7 @@ services:
environment:
- SECRET=secret
- MONGO_URL=mongodb://mongodb:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName":"print","maxPoolSize":1}'
- STORAGE_CONFIG=${STORAGE_CONFIG}
deploy:
resources:
@@ -181,6 +185,7 @@ services:
environment:
- SECRET=secret
- MONGO_URL=mongodb://mongodb:27017
- 'MONGO_OPTIONS={"appName":"sign","maxPoolSize":1}'
- MINIO_ENDPOINT=minio
- MINIO_ACCESS_KEY=minioadmin
- ACCOUNTS_URL=http://account:3000
@@ -201,6 +206,7 @@ services:
- SECRET=secret
- PORT=4007
- MONGO_URL=mongodb://mongodb:27017
- 'MONGO_OPTIONS={"appName":"analytics","maxPoolSize":1}'
- SERVICE_ID=analytics-collector-service
- ACCOUNTS_URL=http://account:3000
- SUPPORT_WORKSPACE=support
+2 -2
View File
@@ -15,6 +15,6 @@
"SIGN_URL": "http://localhost:4006",
"ANALYTICS_COLLECTOR_URL": "http://localhost:4077",
"BRANDING_URL": "/branding.json",
"VERSION": "0.6.266",
"MODEL_VERSION": "0.6.266"
"VERSION": null,
"MODEL_VERSION": null
}
+1
View File
@@ -70,6 +70,7 @@ export class TSpace extends TDoc implements Space {
archived!: boolean
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members)
@Index(IndexKind.Indexed)
members!: Arr<Ref<Account>>
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Owners)
+1 -1
View File
@@ -39,6 +39,6 @@ export function createModel (builder: Builder): void {
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_PREFERENCE,
disabled: [{ modifiedOn: 1 }, { createdOn: 1 }]
disabled: [{ modifiedOn: 1 }, { createdOn: 1 }, { attachedTo: 1 }, { createdOn: -1 }, { modifiedBy: 1 }]
})
}
+1
View File
@@ -148,6 +148,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_TAGS,
disabled: [
{ _class: 1 },
{ modifiedOn: 1 },
{ modifiedBy: 1 },
{ createdBy: 1 },
+1 -2
View File
@@ -1,7 +1,6 @@
// Basic performance metrics suite.
import { MetricsData } from '.'
import { cutObjectArray } from '../utils'
import { FullParamsType, Metrics, ParamsType } from './types'
/**
@@ -35,7 +34,7 @@ function getUpdatedTopResult (
const newValue = {
value: time,
params: cutObjectArray(params)
params
}
if (result.length > 6) {
+5 -1
View File
@@ -487,9 +487,13 @@ export class ApplyOperations extends TxOperations {
extraNotify
)
)) as Promise<TxApplyResult>)
const dnow = Date.now()
if (typeof window === 'object' && window !== null) {
console.log(`measure ${this.measureName}`, dnow - st, 'server time', result.serverTime)
}
return {
result: result.success,
time: Date.now() - st,
time: dnow - st,
serverTime: result.serverTime
}
}
+14 -10
View File
@@ -13,8 +13,9 @@
// limitations under the License.
//
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import { getEmbeddedLabel, IntlString, PlatformError, unknownError } from '@hcengineering/platform'
import { deepEqual } from 'fast-equals'
import { DOMAIN_BENCHMARK } from './benchmark'
import {
Account,
AccountRole,
@@ -46,7 +47,6 @@ import { TxOperations } from './operations'
import { isPredicate } from './predicate'
import { DocumentQuery, FindResult } from './storage'
import { DOMAIN_TX } from './tx'
import { DOMAIN_BENCHMARK } from './benchmark'
function toHex (value: number, chars: number): string {
const result = value.toString(16)
@@ -355,7 +355,6 @@ export class DocManager<T extends Doc> implements IDocManager<T> {
export class RateLimiter {
idCounter: number = 0
processingQueue = new Map<number, Promise<void>>()
last: number = 0
rate: number
@@ -366,21 +365,21 @@ export class RateLimiter {
}
notify: (() => void)[] = []
finished: boolean = false
async exec<T, B extends Record<string, any> = any>(op: (args?: B) => Promise<T>, args?: B): Promise<T> {
const processingId = this.idCounter++
while (this.processingQueue.size >= this.rate) {
if (this.finished) {
throw new PlatformError(unknownError('No Possible to add/exec on finished queue'))
}
while (this.notify.length >= this.rate) {
await new Promise<void>((resolve) => {
this.notify.push(resolve)
})
}
try {
const p = op(args)
this.processingQueue.set(processingId, p as Promise<void>)
return await p
} finally {
this.processingQueue.delete(processingId)
const n = this.notify.shift()
if (n !== undefined) {
n()
@@ -389,7 +388,7 @@ export class RateLimiter {
}
async add<T, B extends Record<string, any> = any>(op: (args?: B) => Promise<T>, args?: B): Promise<void> {
if (this.processingQueue.size < this.rate) {
if (this.notify.length < this.rate) {
void this.exec(op, args)
} else {
await this.exec(op, args)
@@ -397,7 +396,12 @@ export class RateLimiter {
}
async waitProcessing (): Promise<void> {
await Promise.all(this.processingQueue.values())
this.finished = true
while (this.notify.length > 0) {
await new Promise<void>((resolve) => {
this.notify.push(resolve)
})
}
}
}
@@ -3,7 +3,6 @@
"temp/**",
".build/**",
"coverage/**",
"**/*.svelte",
".build/**",
".validate/**",
".format/**",
+22 -10
View File
@@ -31,8 +31,8 @@ export type Loader = (locale: string) => Promise<Record<string, string | Record<
type Messages = Record<string, IntlString | Record<string, IntlString>>
const loaders = new Map<Plugin, Loader>()
const translations = new Map<Plugin, Messages | Status>()
const cache = new Map<IntlString, IntlMessageFormat | Status>()
const translations = new Map<string, Map<Plugin, Messages | Status>>()
const cache = new Map<string, Map<IntlString, IntlMessageFormat | Status>>()
const englishTranslationsForMissing = new Map<Plugin, Messages | Status>()
/**
* @public
@@ -52,10 +52,14 @@ export async function loadPluginStrings (locale: string, force: boolean = false)
cache.clear()
}
for (const [plugin] of loaders) {
let messages = translations.get(plugin)
const localtTanslations = translations.get(locale) ?? new Map<Plugin, Messages | Status<any>>()
if (!translations.has(locale)) {
translations.set(locale, localtTanslations)
}
let messages = localtTanslations.get(plugin)
if (messages === undefined || force) {
messages = await loadTranslationsForComponent(plugin, locale)
translations.set(plugin, messages)
localtTanslations.set(plugin, messages)
}
}
}
@@ -83,10 +87,14 @@ async function loadTranslationsForComponent (plugin: Plugin, locale: string): Pr
async function getTranslation (id: _IdInfo, locale: string): Promise<IntlString | Status | undefined> {
try {
let messages = translations.get(id.component)
const localtTanslations = translations.get(locale) ?? new Map<Plugin, Messages | Status<any>>()
if (!translations.has(locale)) {
translations.set(locale, localtTanslations)
}
let messages = localtTanslations.get(id.component)
if (messages === undefined) {
messages = await loadTranslationsForComponent(id.component, locale)
translations.set(id.component, messages)
localtTanslations.set(id.component, messages)
}
if (messages instanceof Status) {
return messages
@@ -127,7 +135,11 @@ export async function translate<P extends Record<string, any>> (
language?: string
): Promise<string> {
const locale = language ?? getMetadata(platform.metadata.locale) ?? 'en'
const compiled = cache.get(message)
const localCache = cache.get(locale) ?? new Map<IntlString, IntlMessageFormat | Status>()
if (!cache.has(locale)) {
cache.set(locale, localCache)
}
const compiled = localCache.get(message)
if (compiled !== undefined) {
if (compiled instanceof Status) {
@@ -142,16 +154,16 @@ export async function translate<P extends Record<string, any>> (
}
const translation = (await getTranslation(id, locale)) ?? message
if (translation instanceof Status) {
cache.set(message, translation)
localCache.set(message, translation)
return message
}
const compiled = new IntlMessageFormat(translation, locale, undefined, { ignoreTag: true })
cache.set(message, compiled)
localCache.set(message, compiled)
return compiled.format(params)
} catch (err) {
const status = unknownError(err)
await setPlatformStatus(status)
cache.set(message, status)
localCache.set(message, status)
return message
}
}
@@ -190,6 +190,7 @@
{size}
icon={IconAdd}
showTooltip={{ label: create.label }}
dataId={'btnAdd'}
on:click={onCreate}
disabled={readonly || loading}
/>
@@ -13,18 +13,18 @@
// limitations under the License.
-->
<script lang="ts">
import { type Blob } from '@hcengineering/core'
import { type Blob, type Ref } from '@hcengineering/core'
import { Button, Component, Label, resizeObserver, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import presentation from '../plugin'
import { getPreviewType, previewTypes } from '../file'
import { BlobMetadata, FilePreviewExtension } from '../types'
import { getFileUrl } from '../utils'
import { getBlobSrcFor } from '../preview'
export let file: Blob
export let file: Ref<Blob>
export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined
export let props: Record<string, any> = {}
export let fit: boolean = false
@@ -35,7 +35,7 @@
$: parentHeight = ($deviceInfo.docHeight * 80) / 100
let previewType: FilePreviewExtension | undefined = undefined
$: void getPreviewType(file.contentType, $previewTypes).then((res) => {
$: void getPreviewType(contentType, $previewTypes).then((res) => {
previewType = res
})
@@ -75,7 +75,7 @@
}
$: updateHeight(parentWidth, parentHeight, previewType, metadata)
$: audio = previewType && Array.isArray(previewType) && previewType[0].contentType === 'audio/*'
$: srcRef = getBlobSrcFor(file, name)
$: srcRef = getFileUrl(file, name)
</script>
<div
@@ -90,10 +90,7 @@
<Label label={presentation.string.FailedToPreview} />
</div>
{:else if previewType !== undefined}
<Component
is={previewType.component}
props={{ value: file, name, contentType: file.contentType, metadata, ...props, fit }}
/>
<Component is={previewType.component} props={{ value: file, name, contentType, metadata, ...props, fit }} />
{:else}
<div class="flex-col items-center flex-gap-3">
<Label label={presentation.string.ContentTypeNotSupported} />
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import core, { type Blob, type Ref } from '@hcengineering/core'
import { type Blob, type Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Button, Dialog, tooltip } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
@@ -21,15 +21,15 @@
import presentation from '../plugin'
import { BlobMetadata } from '../types'
import { getClient } from '../utils'
import { getBlobSrcFor } from '../preview'
import { getClient, getFileUrl } from '../utils'
import ActionContext from './ActionContext.svelte'
import FilePreview from './FilePreview.svelte'
import Download from './icons/Download.svelte'
export let file: Blob | Ref<Blob> | undefined
export let file: Ref<Blob> | undefined
export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined
export let props: Record<string, any> = {}
@@ -53,14 +53,7 @@
return ext.substring(0, 4).toUpperCase()
}
let blob: Blob | undefined = undefined
$: void fetchBlob(file)
async function fetchBlob (file: Blob | Ref<Blob> | undefined): Promise<void> {
blob = typeof file === 'string' ? await client.findOne(core.class.Blob, { _id: file }) : file
}
$: srcRef = getBlobSrcFor(blob, name)
$: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
</script>
<ActionContext context={{ mode: 'browser' }} />
@@ -101,8 +94,8 @@
{/await}
</svelte:fragment>
{#if blob !== undefined}
<FilePreview file={blob} {name} {metadata} {props} fit />
{#if file}
<FilePreview {file} {contentType} {name} {metadata} {props} fit />
{/if}
</Dialog>
@@ -17,11 +17,11 @@
import type { Blob, Ref } from '@hcengineering/core'
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import presentation, { getBlobSrcFor } from '..'
import presentation, { getFileUrl } from '..'
import ActionContext from './ActionContext.svelte'
import Download from './icons/Download.svelte'
export let file: Blob | Ref<Blob> | undefined
export let file: Ref<Blob> | undefined
export let name: string
export let contentType: string | undefined
// export let popupOptions: PopupOptions
@@ -45,7 +45,7 @@
})
let download: HTMLAnchorElement
$: srcRef = getBlobSrcFor(file, name)
$: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
$: isImage = contentType !== undefined && contentType.startsWith('image/')
+17
View File
@@ -9,6 +9,7 @@ import {
type FindResult,
type Hierarchy,
type ModelDb,
type QuerySelector,
type Ref,
type SearchOptions,
type SearchQuery,
@@ -330,6 +331,22 @@ export class OptimizeQueryMiddleware extends BasePresentationMiddleware implemen
const fQuery = { ...query }
const fOptions = { ...options }
this.optimizeQuery<T>(fQuery, fOptions)
// Immidiate response queries, if have some $in with empty list.
for (const [k, v] of Object.entries(fQuery)) {
if (typeof v === 'object' && v != null) {
const vobj = v as QuerySelector<any>
if (vobj.$in != null && vobj.$in.length === 0) {
// Emopty in, will always return []
return toFindResult([], 0)
} else if (vobj.$in != null && vobj.$in.length === 1 && Object.keys(vobj).length === 1) {
;(fQuery as any)[k] = vobj.$in[0]
} else if (vobj.$nin != null && vobj.$nin.length === 1 && Object.keys(vobj).length === 1) {
;(fQuery as any)[k] = { $ne: vobj.$nin[0] }
}
}
}
return await this.provideFindAll(_class, fQuery, fOptions)
}
+18 -140
View File
@@ -1,99 +1,36 @@
import type { Blob, BlobLookup, Ref } from '@hcengineering/core'
import core, { concatLink } from '@hcengineering/core'
import type { Blob, Ref } from '@hcengineering/core'
import { concatLink } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { getBlobHref, getClient, getCurrentWorkspaceUrl, getFileUrl } from '.'
import { getCurrentWorkspaceUrl, getFileUrl } from '.'
import presentation from './plugin'
export interface ProviderPreviewConfig {
// Identifier of provider
// If set to '' could be applied to any provider, for example to exclude some 'image/gif' etc from being processing with providers.
providerId: string
// Preview url
// If '' preview is disabled for config.
previewUrl: string
// Content type markers, will check by containts, if passed, only allow to be used with matched content types.
contentTypes?: string[]
}
export interface PreviewConfig {
default?: ProviderPreviewConfig
previewers: Record<string, ProviderPreviewConfig[]>
previewUrl: string
}
const defaultPreview = (): ProviderPreviewConfig => ({
providerId: '',
previewUrl: `/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size`
})
const defaultPreview = (): string => `/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size`
/**
*
* PREVIEW_CONFIG env variable format.
* A `;` separated list of triples, providerName|previewUrl|supportedFormats.
- providerName - a provider name should be same as in Storage configuration.
It coult be empty and it will match by content types.
- previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values.
- supportedFormats - a `,` separated list of file extensions.
- contentTypes - a ',' separated list of content type patterns.
* previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values.
*/
export function parsePreviewConfig (config?: string): PreviewConfig | undefined {
if (config === undefined) {
return
}
const result: PreviewConfig = { previewers: {} }
const nolineData = config
.split('\n')
.map((it) => it.trim())
.join(';')
const configs = nolineData.split(';')
for (const c of configs) {
if (c === '') {
continue // Skip empty lines
}
const [provider, url, contentTypes] = c.split('|').map((it) => it.trim())
const p: ProviderPreviewConfig = {
providerId: provider,
previewUrl: url,
// Allow preview only for images by default
contentTypes:
contentTypes !== undefined
? contentTypes
.split(',')
.map((it) => it.trim())
.filter((it) => it !== '')
: ['image/']
}
if (provider === '*') {
result.default = p
} else {
result.previewers[provider] = [...(result.previewers[provider] ?? []), p]
}
}
return result
return { previewUrl: config }
}
export function getPreviewConfig (): PreviewConfig {
return (
(getMetadata(presentation.metadata.PreviewConfig) as PreviewConfig) ?? {
default: defaultPreview(),
previewers: {
'': [
{
providerId: '',
contentTypes: ['image/gif', 'image/apng', 'image/svg'], // Disable gif and apng format preview.
previewUrl: ''
}
]
}
previewUrl: defaultPreview()
}
)
}
export async function getBlobRef (
blob: Blob | undefined,
file: Ref<Blob>,
name?: string,
width?: number
@@ -101,74 +38,30 @@ export async function getBlobRef (
src: string
srcset: string
}> {
let _blob = blob as BlobLookup
if (_blob === undefined) {
_blob = (await getClient().findOne(core.class.Blob, { _id: file })) as BlobLookup
}
return {
src: _blob?.downloadUrl ?? getFileUrl(file, name),
srcset: _blob !== undefined ? getSrcSet(_blob, width) : ''
src: getFileUrl(file, name),
srcset: getSrcSet(file, width)
}
}
export async function getBlobSrcSet (_blob: Blob | undefined, file: Ref<Blob>, width?: number): Promise<string> {
if (_blob === undefined) {
_blob = await getClient().findOne(core.class.Blob, { _id: file })
}
return _blob !== undefined ? getSrcSet(_blob, width) : ''
export async function getBlobSrcSet (file: Ref<Blob>, width?: number): Promise<string> {
return getSrcSet(file, width)
}
/**
* Select content provider based on content type.
*/
export function selectProvider (
blob: Blob,
providers: Array<ProviderPreviewConfig | undefined>
): ProviderPreviewConfig | undefined {
const isMatched = (it: ProviderPreviewConfig): boolean =>
it.contentTypes === undefined || it.contentTypes.some((e) => blob.contentType === e || blob.contentType.includes(e))
let candidate: ProviderPreviewConfig | undefined
for (const p of providers) {
if (p !== undefined && isMatched(p)) {
if (p.previewUrl === '') {
// we found one disable config line, so return it.
return p
}
candidate = p
}
}
return candidate
export function getSrcSet (_blob: Ref<Blob>, width?: number): string {
return blobToSrcSet(getPreviewConfig(), _blob, width)
}
export function getSrcSet (_blob: Blob, width?: number): string {
const blob = _blob as BlobLookup
const c = getPreviewConfig()
// Select providers from
const cfg = selectProvider(blob, [...(c.previewers[_blob.provider] ?? []), ...(c.previewers[''] ?? []), c.default])
if (cfg === undefined || cfg.previewUrl === '') {
return '' // No previewer is available for blob
}
return blobToSrcSet(cfg, blob, width)
}
function blobToSrcSet (
cfg: ProviderPreviewConfig,
blob: { _id: Ref<Blob>, downloadUrl?: string },
width: number | undefined
): string {
function blobToSrcSet (cfg: PreviewConfig, blob: Ref<Blob>, width: number | undefined): string {
let url = cfg.previewUrl.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceUrl()))
const downloadUrl = blob.downloadUrl ?? getFileUrl(blob._id)
const downloadUrl = getFileUrl(blob)
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
if (!url.includes('://')) {
url = concatLink(frontUrl ?? '', url)
}
url = url.replaceAll(':downloadFile', encodeURIComponent(downloadUrl))
url = url.replaceAll(':blobId', encodeURIComponent(blob._id))
url = url.replaceAll(':blobId', encodeURIComponent(blob))
let result = ''
const fu = url
@@ -187,24 +80,9 @@ function blobToSrcSet (
return result
}
export async function getBlobSrcFor (blob: Blob | Ref<Blob> | undefined, name?: string): Promise<string> {
return blob === undefined
? ''
: typeof blob === 'string'
? await getBlobHref(undefined, blob, name)
: await getBlobHref(blob, blob._id)
}
/***
* @deprecated, please use Blob direct operations.
*/
export function getFileSrcSet (_blob: Ref<Blob>, width?: number): string {
const cfg = getPreviewConfig()
return blobToSrcSet(
cfg.default ?? defaultPreview(),
{
_id: _blob
},
width
)
return blobToSrcSet(getPreviewConfig(), _blob, width)
}
+5 -14
View File
@@ -24,7 +24,6 @@ import core, {
type AnyAttribute,
type ArrOf,
type AttachedDoc,
type BlobLookup,
type Class,
type Client,
type Collection,
@@ -450,18 +449,6 @@ export function createQuery (dontDestroy?: boolean): LiveQuery {
return new LiveQuery(dontDestroy)
}
export async function getBlobHref (
_blob: PlatformBlob | undefined,
file: Ref<PlatformBlob>,
filename?: string
): Promise<string> {
let blob = _blob as BlobLookup
if (blob?.downloadUrl === undefined) {
blob = (await getClient().findOne(core.class.Blob, { _id: file })) as BlobLookup
}
return blob?.downloadUrl ?? getFileUrl(file, filename)
}
export function getCurrentWorkspaceUrl (): string {
const wsId = get(workspaceId)
if (wsId == null) {
@@ -700,7 +687,11 @@ export function isAdminUser (): boolean {
}
export function isSpace (space: Doc): space is Space {
return getClient().getHierarchy().isDerived(space._class, core.class.Space)
return isSpaceClass(space._class)
}
export function isSpaceClass (_class: Ref<Class<Doc>>): boolean {
return getClient().getHierarchy().isDerived(_class, core.class.Space)
}
export function setPresentationCookie (token: string, workspaceId: string): void {
+1 -38
View File
@@ -13,17 +13,7 @@
// limitations under the License.
//
import {
type Blob,
type Branding,
type DocumentUpdate,
type MeasureContext,
type Ref,
type StorageIterator,
type WorkspaceId,
type WorkspaceIdWithUrl
} from '@hcengineering/core'
import type { BlobLookup } from '@hcengineering/core/src/classes'
import { type Blob, type MeasureContext, type StorageIterator, type WorkspaceId } from '@hcengineering/core'
import { type Readable } from 'stream'
export type ListBlobResult = Omit<Blob, 'contentType' | 'version'>
@@ -38,11 +28,6 @@ export interface BlobStorageIterator {
close: () => Promise<void>
}
export interface BlobLookupResult {
lookups: BlobLookup[]
updates?: Map<Ref<Blob>, DocumentUpdate<BlobLookup>>
}
export interface BucketInfo {
name: string
delete: () => Promise<void>
@@ -50,11 +35,6 @@ export interface BucketInfo {
}
export interface StorageAdapter {
// If specified will limit a blobs available to put into selected provider.
// A set of content type patterns supported by this storage provider.
// If not defined, will be suited for any other content types.
contentTypes?: string[]
initialize: (ctx: MeasureContext, workspaceId: WorkspaceId) => Promise<void>
close: () => Promise<void>
@@ -84,14 +64,6 @@ export interface StorageAdapter {
offset: number,
length?: number
) => Promise<Readable>
// Lookup will extend Blob with lookup information.
lookup: (
ctx: MeasureContext,
workspaceId: WorkspaceIdWithUrl,
branding: Branding | null,
docs: Blob[]
) => Promise<BlobLookupResult>
}
export interface StorageAdapterEx extends StorageAdapter {
@@ -189,15 +161,6 @@ export class DummyStorageAdapter implements StorageAdapter, StorageAdapterEx {
): Promise<UploadedObjectInfo> {
throw new Error('not implemented')
}
async lookup (
ctx: MeasureContext,
workspaceId: WorkspaceIdWithUrl,
branding: Branding | null,
docs: Blob[]
): Promise<BlobLookupResult> {
return { lookups: [] }
}
}
export function createDummyStorageAdapter (): StorageAdapter {
+5 -1
View File
@@ -28,6 +28,10 @@ export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
if (options === undefined) {
return {}
}
if (options.label === undefined && options.component === undefined) {
// No tooltip
return {}
}
let opt = options
const show = (): void => {
const shown = !!(storedValue.label !== undefined || storedValue.component !== undefined)
@@ -113,7 +117,7 @@ export function showTooltip (
props,
anchor,
onUpdate,
kind,
kind: kind ?? 'tooltip',
keys,
type: 'tooltip'
}
+2 -2
View File
@@ -1,5 +1,5 @@
import activity, { type ActivityMessage, type SavedMessage } from '@hcengineering/activity'
import { type Ref, SortingOrder, type WithLookup } from '@hcengineering/core'
import core, { type Ref, SortingOrder, type WithLookup } from '@hcengineering/core'
import { writable } from 'svelte/store'
import { createQuery, getClient } from '@hcengineering/presentation'
@@ -14,7 +14,7 @@ export function loadSavedMessages (): void {
if (client !== undefined) {
savedMessagesQuery.query(
activity.class.SavedMessage,
{},
{ space: core.space.Workspace },
(res) => {
savedMessagesStore.set(res.filter(({ $lookup }) => $lookup?.attachedTo !== undefined))
},
@@ -13,20 +13,26 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityExtension, ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import activity, {
ActivityExtension,
ActivityMessage,
ActivityReference,
DisplayActivityMessage,
WithReferences
} from '@hcengineering/activity'
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
import { createQuery, getClient, isSpace } from '@hcengineering/presentation'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Grid, Label, Spinner, location, Lazy } from '@hcengineering/ui'
import { onDestroy, onMount } from 'svelte'
import ActivityExtensionComponent from './ActivityExtension.svelte'
import ActivityFilter from './ActivityFilter.svelte'
import { combineActivityMessages } from '../activityMessagesUtils'
import { canGroupMessages, getMessageFromLoc } from '../utils'
import { combineActivityMessages, sortActivityMessages } from '../activityMessagesUtils'
import { canGroupMessages, getMessageFromLoc, getSpace } from '../utils'
import ActivityMessagePresenter from './activity-message/ActivityMessagePresenter.svelte'
import { messageInFocus } from '../activity'
export let object: Doc
export let object: WithReferences<Doc>
export let showCommenInput: boolean = true
export let transparent: boolean = false
export let focusIndex: number = -1
@@ -34,12 +40,17 @@
const client = getClient()
const activityMessagesQuery = createQuery()
const refsQuery = createQuery()
let extensions: ActivityExtension[] = []
let filteredMessages: DisplayActivityMessage[] = []
let activityMessages: ActivityMessage[] = []
let isLoading = false
let allMessages: ActivityMessage[] = []
let messages: ActivityMessage[] = []
let refs: ActivityReference[] = []
let isMessagesLoading = false
let isRefsLoading = true
let activityBox: HTMLElement | undefined
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
@@ -163,16 +174,38 @@
extensions = res
})
// Load references from other spaces separately because they can have any different spaces
$: if ((object.references ?? 0) > 0) {
refsQuery.query(
activity.class.ActivityReference,
{ attachedTo: object._id, space: { $ne: getSpace(object) } },
(res) => {
refs = res
isRefsLoading = false
},
{
sort: {
createdOn: SortingOrder.Ascending
}
}
)
} else {
isRefsLoading = false
refsQuery.unsubscribe()
}
$: allMessages = sortActivityMessages(messages.concat(refs))
async function updateActivityMessages (objectId: Ref<Doc>, order: SortingOrder): Promise<void> {
isLoading = true
isMessagesLoading = true
const res = activityMessagesQuery.query(
activity.class.ActivityMessage,
{ attachedTo: objectId, space: isSpace(object) ? object._id : object.space },
{ attachedTo: objectId, space: getSpace(object) },
(result: ActivityMessage[]) => {
void combineActivityMessages(result, order).then((messages) => {
activityMessages = messages
isLoading = false
void combineActivityMessages(result, order).then((res) => {
messages = res
isMessagesLoading = false
})
},
{
@@ -182,10 +215,11 @@
}
)
if (!res) {
isLoading = false
isMessagesLoading = false
}
}
$: isLoading = isMessagesLoading || isRefsLoading
$: areMessagesLoaded = !isLoading && filteredMessages.length > 0
$: if (activityBox && areMessagesLoaded) {
@@ -206,7 +240,7 @@
{/if}
</span>
<ActivityFilter
messages={activityMessages}
messages={allMessages}
{object}
on:update={(e) => {
filteredMessages = e.detail
@@ -16,8 +16,8 @@
import { Label, tooltip } from '@hcengineering/ui'
import { DocNotifyContext } from '@hcengineering/notification'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Doc } from '@hcengineering/core'
import { getDocLinkTitle, getDocTitle, ObjectIcon } from '@hcengineering/view-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import contact from '@hcengineering/contact'
@@ -25,54 +25,44 @@
import ActivityMessagePreview from './ActivityMessagePreview.svelte'
export let context: DocNotifyContext
export let object: ActivityMessage
const client = getClient()
const hierarchy = client.getHierarchy()
const parentQuery = createQuery()
let parentMessage: ActivityMessage | undefined = undefined
let title: string | undefined = undefined
let object: Doc | undefined = undefined
$: parentQuery.query(activity.class.ActivityMessage, { _id: context.attachedTo as Ref<ActivityMessage> }, (res) => {
parentMessage = res[0]
})
$: parentMessage &&
client.findOne(parentMessage.attachedToClass, { _id: parentMessage.attachedTo }).then((res) => {
object = res
})
let doc: Doc | undefined = undefined
$: object &&
getDocLinkTitle(client, object._id, object._class, object).then((res) => {
client.findOne(object.attachedToClass, { _id: object.attachedTo, space: object.space }).then((res) => {
doc = res
})
$: doc &&
getDocLinkTitle(client, doc._id, doc._class, doc).then((res) => {
title = res
})
</script>
{#if parentMessage}
<span class="flex-presenter flex-gap-1 font-semi-bold">
<Label label={(parentMessage?.replies ?? 0) > 0 ? activity.string.Thread : activity.string.Message} />
{#if title}
<span class="lower">
<Label label={activity.string.In} />
</span>
{#if object}
{#await getDocTitle(client, object._id, object._class, object) then tooltipLabel}
<span
class="flex-presenter flex-gap-0-5"
use:tooltip={tooltipLabel ? { label: getEmbeddedLabel(tooltipLabel) } : undefined}
>
<ObjectIcon
value={object}
size={hierarchy.isDerived(object._class, contact.class.Person) ? 'tiny' : 'small'}
/>
{title}
</span>
{/await}
{/if}
<span class="flex-presenter flex-gap-1 font-semi-bold">
<Label label={(object?.replies ?? 0) > 0 ? activity.string.Thread : activity.string.Message} />
{#if title}
<span class="lower">
<Label label={activity.string.In} />
</span>
{#if doc}
{#await getDocTitle(client, doc._id, doc._class, doc) then tooltipLabel}
<span
class="flex-presenter flex-gap-0-5"
use:tooltip={tooltipLabel ? { label: getEmbeddedLabel(tooltipLabel) } : undefined}
>
<ObjectIcon value={doc} size={hierarchy.isDerived(doc._class, contact.class.Person) ? 'tiny' : 'small'} />
{title}
</span>
{/await}
{/if}
</span>
<span class="font-normal">
<ActivityMessagePreview value={parentMessage} readonly type="content-only" />
</span>
{/if}
{/if}
</span>
<span class="font-normal">
<ActivityMessagePreview value={object} {doc} readonly type="content-only" />
</span>
@@ -21,6 +21,7 @@
import activity from '../../plugin'
export let doc: Doc | undefined
export let value: DisplayActivityMessage
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
@@ -44,7 +45,8 @@
type,
readonly,
actions,
space
space,
doc
}}
on:click
/>
@@ -13,22 +13,10 @@
// limitations under the License.
-->
<script lang="ts">
import { createQuery, MessageViewer } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import activity, { ActivityReference } from '@hcengineering/activity'
import { MessageViewer } from '@hcengineering/presentation'
import { ActivityReference } from '@hcengineering/activity'
export let _id: Ref<ActivityReference> | undefined = undefined
export let value: ActivityReference | undefined = undefined
const query = createQuery()
$: if (value === undefined && _id !== undefined) {
query.query(activity.class.ActivityReference, { _id }, (res) => {
value = res.shift()
})
} else {
query.unsubscribe()
}
</script>
{#if value}
@@ -16,12 +16,12 @@
import type { Doc } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { DocReferencePresenter } from '@hcengineering/view-resources'
import activity from '../../plugin'
import { isActivityMessage } from '../../activityMessagesUtils'
import view from '@hcengineering/view'
import { Icon, Label } from '@hcengineering/ui'
import activity from '../../plugin'
import { isActivityMessage } from '../../activityMessagesUtils'
export let value: Doc | undefined
const client = getClient()
@@ -31,7 +31,7 @@
$: showParent = isActivityMessage(value)
$: isActivityMessage(value) &&
client.findOne(value.attachedToClass, { _id: value.attachedTo }).then((res) => {
client.findOne(value.attachedToClass, { _id: value.attachedTo, space: value.space }).then((res) => {
parentObject = res
})
</script>
@@ -88,13 +88,17 @@
attributeModel = model
})
async function getParentMessage (_class: Ref<Class<Doc>>, _id: Ref<Doc>): Promise<ActivityMessage | undefined> {
async function getParentMessage (
_class: Ref<Class<Doc>>,
_id: Ref<Doc>,
space: Ref<Space>
): Promise<ActivityMessage | undefined> {
if (hierarchy.isDerived(_class, activity.class.ActivityMessage)) {
return await client.findOne(activity.class.ActivityMessage, { _id: _id as Ref<ActivityMessage> })
return await client.findOne(activity.class.ActivityMessage, { _id: _id as Ref<ActivityMessage>, space })
}
}
$: void getParentMessage(value.attachedToClass, value.attachedTo).then((res) => {
$: void getParentMessage(value.attachedToClass, value.attachedTo, value.space).then((res) => {
parentMessage = res as DisplayActivityMessage
})
@@ -150,6 +154,7 @@
const _id = parentMessage ? parentMessage.attachedTo : message.attachedTo
const _class = parentMessage ? parentMessage.attachedToClass : message.attachedToClass
const space = parentMessage ? parentMessage.space : message.space
if (doc !== undefined && doc._id === _id) {
parentObject = doc
@@ -163,7 +168,7 @@
return
}
parentObjectQuery.query(_class, { _id }, (res) => {
parentObjectQuery.query(_class, { _id, space }, (res) => {
parentObject = res[0]
})
}
@@ -32,6 +32,7 @@
import DocUpdateMessageContent from './DocUpdateMessageContent.svelte'
import DocUpdateMessageAttributes from './DocUpdateMessageAttributes.svelte'
export let doc: Doc | undefined
export let value: DisplayDocUpdateMessage
export let readonly = false
export let type: ActivityMessagePreviewType = 'full'
@@ -65,15 +66,20 @@
attributeModel = model
})
$: viewlet?.component && loadObject(value.objectId, value.objectClass)
$: viewlet?.component && loadObject(value.objectId, value.objectClass, value.space, doc)
async function loadObject (_id: Ref<Doc>, _class: Ref<Class<Doc>>, space: Ref<Space>, doc?: Doc): Promise<void> {
if (doc?._id === _id) {
object = doc
return
}
async function loadObject (_id: Ref<Doc>, _class: Ref<Class<Doc>>): Promise<void> {
const isObjectRemoved = await checkIsObjectRemoved(client, _id, _class)
if (isObjectRemoved) {
object = await buildRemovedDoc(client, _id, _class)
} else {
objectQuery.query(_class, { _id }, (res) => {
objectQuery.query(_class, { _id, space }, (res) => {
object = res[0]
})
}
@@ -14,18 +14,19 @@
-->
<script lang="ts">
import { Reaction } from '@hcengineering/activity'
import { Ref } from '@hcengineering/core'
import { Ref, Space } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import activity from '../../plugin'
export let _id: Ref<Reaction>
export let space: Ref<Space>
export let value: Reaction | undefined = undefined
const query = createQuery()
$: value === undefined &&
query.query(activity.class.Reaction, { _id }, (res) => {
query.query(activity.class.Reaction, { _id, space }, (res) => {
value = res[0]
})
</script>
@@ -16,7 +16,7 @@
import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
import { createQuery, getClient } from '@hcengineering/presentation'
import { updateDocReactions } from '../../utils'
import { getSpace, updateDocReactions } from '../../utils'
import Reactions from './Reactions.svelte'
export let object: ActivityMessage | undefined
@@ -30,9 +30,13 @@
$: hasReactions = object?.reactions && object.reactions > 0
$: if (object && hasReactions) {
reactionsQuery.query(activity.class.Reaction, { attachedTo: object._id }, (res: Reaction[]) => {
reactions = res
})
reactionsQuery.query(
activity.class.Reaction,
{ attachedTo: object._id, space: getSpace(object) },
(res: Reaction[]) => {
reactions = res
}
)
} else {
reactionsQuery.unsubscribe()
}
@@ -37,7 +37,7 @@
$: if (message && hasReactions) {
reactionsQuery.query(
activity.class.Reaction,
{ attachedTo: message._id },
{ attachedTo: message._id, space: message.space },
(res: Reaction[]) => {
reactions = res
+14 -3
View File
@@ -1,6 +1,13 @@
import type { ActivityMessage, Reaction } from '@hcengineering/activity'
import core, { getCurrentAccount, isOtherHour, type Doc, type Ref, type TxOperations } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import core, {
getCurrentAccount,
isOtherHour,
type Doc,
type Ref,
type TxOperations,
type Space
} from '@hcengineering/core'
import { getClient, isSpace } from '@hcengineering/presentation'
import {
EmojiPopup,
closePopup,
@@ -58,7 +65,7 @@ export async function addReactionAction (
const client = getClient()
const reactions: Reaction[] =
(message.reactions ?? 0) > 0
? await client.findAll<Reaction>(activity.class.Reaction, { attachedTo: message._id })
? await client.findAll<Reaction>(activity.class.Reaction, { attachedTo: message._id, space: message.space })
: []
const element = getEventPositionElement(ev)
@@ -169,3 +176,7 @@ export function shouldScrollToActivity (): boolean {
const loc = getCurrentResolvedLocation()
return getMessageFromLoc(loc) !== undefined
}
export function getSpace (doc: Doc): Ref<Space> {
return isSpace(doc) ? doc._id : doc.space
}
+4
View File
@@ -235,6 +235,10 @@ export interface UserMentionInfo extends AttachedDoc {
content: string
}
export type WithReferences<T extends Doc> = T & {
references?: number
}
/**
* @public
*/
@@ -18,7 +18,7 @@
import {
FilePreviewPopup,
canPreviewFile,
getBlobHref,
getFileUrl,
getPreviewAlignment,
previewTypes
} from '@hcengineering/presentation'
@@ -71,7 +71,8 @@
showPopup(
FilePreviewPopup,
{
file: attachment.$lookup?.file ?? attachment.file,
file: attachment.file,
contentType: attachment.type,
name: attachment.name,
metadata: attachment.metadata
},
@@ -130,32 +131,30 @@
</script>
<div class="flex">
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
<a
class="mr-1 flex-row-center gap-2 p-1"
{href}
download={attachment.name}
bind:this={download}
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
on:click|stopPropagation
>
{#if canPreview}
<ActionIcon
icon={IconOpen}
size={'medium'}
action={(evt) => {
showPreview(evt)
}}
/>
{/if}
<a
class="mr-1 flex-row-center gap-2 p-1"
href={getFileUrl(attachment.file, attachment.name)}
download={attachment.name}
bind:this={download}
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
on:click|stopPropagation
>
{#if canPreview}
<ActionIcon
icon={FileDownload}
icon={IconOpen}
size={'medium'}
action={() => {
download.click()
action={(evt) => {
showPreview(evt)
}}
/>
</a>
{/await}
{/if}
<ActionIcon
icon={FileDownload}
size={'medium'}
action={() => {
download.click()
}}
/>
</a>
<ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} />
</div>
@@ -48,11 +48,6 @@
},
(res) => {
resAttachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)
} else {
@@ -15,7 +15,7 @@
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core'
import { FilePreviewPopup, getBlobHref } from '@hcengineering/presentation'
import { FilePreviewPopup, getFileUrl } from '@hcengineering/presentation'
import { closeTooltip, showPopup } from '@hcengineering/ui'
import filesize from 'filesize'
import { getType } from '../utils'
@@ -45,41 +45,27 @@
showPopup(
FilePreviewPopup,
{
file: value.$lookup?.file ?? value.file,
file: value.file,
contentType: value.type,
name: value.name,
metadata: value.metadata
},
isImage(value.type) ? 'centered' : 'float'
)
}
$: src = getFileUrl(value.file, value.name)
</script>
<div class="gridCellOverlay">
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then src}
<div class="gridCell">
{#if isImage(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="cellImagePreview" on:click={openAttachment}>
<img class={'img-fit'} {src} alt={value.name} />
</div>
{:else}
<div class="cellMiscPreview">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-center extensionIcon" on:click={openAttachment}>
{extensionIconLabel(value.name)}
</div>
{:else}
<a class="no-line" href={src} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a>
{/if}
</div>
{/if}
<div class="cellInfo">
<div class="gridCell">
{#if isImage(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="cellImagePreview" on:click={openAttachment}>
<img class={'img-fit'} {src} alt={value.name} />
</div>
{:else}
<div class="cellMiscPreview">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -91,24 +77,38 @@
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a>
{/if}
<div class="eCellInfoData">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eCellInfoFilename" on:click={openAttachment}>
{trimFilename(value.name)}
</div>
{:else}
<div class="eCellInfoFilename">
<a href={src} download={value.name}>{trimFilename(value.name)}</a>
</div>
{/if}
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
</div>
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
</div>
{/if}
<div class="cellInfo">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-center extensionIcon" on:click={openAttachment}>
{extensionIconLabel(value.name)}
</div>
{:else}
<a class="no-line" href={src} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a>
{/if}
<div class="eCellInfoData">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eCellInfoFilename" on:click={openAttachment}>
{trimFilename(value.name)}
</div>
{:else}
<div class="eCellInfoFilename">
<a href={src} download={value.name}>{trimFilename(value.name)}</a>
</div>
{/if}
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
</div>
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
</div>
{/await}
</div>
</div>
<style lang="scss">
@@ -107,7 +107,7 @@
</script>
<div class="container" style="width:{toStyle(dimensions.width)}; height:{toStyle(dimensions.height)}">
{#await getBlobRef(value.$lookup?.file, value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
{#await getBlobRef(value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
<img
src={blobSrc.src}
style:object-fit={getObjectFit(dimensions)}
@@ -43,11 +43,6 @@
},
(res) => {
docs = res
},
{
lookup: {
file: core.class.Blob
}
}
)
@@ -83,7 +83,8 @@
showPopup(
FilePreviewPopup,
{
file: value.$lookup?.file ?? value.file,
file: value.file,
contentType: value.type,
name: value.name,
metadata: value.metadata
},
@@ -114,7 +115,7 @@
{:else}
<div class="flex-row-center attachment-container">
{#if value}
{#await getBlobRef(value.$lookup?.file, value.file, value.name, sizeToWidth('large')) then valueRef}
{#await getBlobRef(value.file, value.name, sizeToWidth('large')) then valueRef}
<a
class="no-line"
style:flex-shrink={0}
@@ -51,7 +51,7 @@
if (listProvider !== undefined) listProvider.updateFocus(value)
const popupInfo = showPopup(
FilePreviewPopup,
{ file: value.$lookup?.file ?? value.file, name: value.name },
{ file: value.file, name: value.name, contentType: value.type },
value.type.startsWith('image/') ? 'centered' : 'float'
)
dispatch('open', popupInfo.id)
@@ -121,11 +121,6 @@
(res) => {
originalAttachments = new Set(res.map((p) => p._id))
attachments = toIdMap(res)
},
{
lookup: {
file: core.class.Blob
}
}
)
} else {
@@ -15,7 +15,7 @@
<script lang="ts">
import attachment, { Attachment, BlobMetadata } from '@hcengineering/attachment'
import contact from '@hcengineering/contact'
import core, { Account, Doc, Ref, generateId, type Blob } from '@hcengineering/core'
import { Account, Doc, Ref, generateId, type Blob } from '@hcengineering/core'
import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
FileOrBlob,
@@ -25,7 +25,6 @@
getFileMetadata,
uploadFile
} from '@hcengineering/presentation'
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
import textEditor, { type RefAction, type TextEditorHandler } from '@hcengineering/text-editor'
import {
AttachIcon,
@@ -38,6 +37,7 @@
import { AnySvelteComponent, getEventPositionElement, getPopupPositionElement, navigate } from '@hcengineering/ui'
import { uploadFiles } from '@hcengineering/uploader'
import view from '@hcengineering/view'
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
import AttachmentsGrid from './AttachmentsGrid.svelte'
@@ -106,11 +106,6 @@
},
(res) => {
attachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)
@@ -13,23 +13,23 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte'
import { Attachment } from '@hcengineering/attachment'
import core, { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap, type Blob } from '@hcengineering/core'
import { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap, type Blob } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
createQuery,
DraftController,
deleteFile,
DraftController,
draftsStore,
getClient,
getFileMetadata,
uploadFile
} from '@hcengineering/presentation'
import textEditor, { type RefAction } from '@hcengineering/text-editor'
import { EmptyMarkup } from '@hcengineering/text'
import textEditor, { type RefAction } from '@hcengineering/text-editor'
import { AttachIcon, StyledTextBox } from '@hcengineering/text-editor-resources'
import { ButtonSize } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte'
import attachment from '../plugin'
import AttachmentsGrid from './AttachmentsGrid.svelte'
@@ -127,11 +127,6 @@
originalAttachments = new Set(res.map((p) => p._id))
attachments = toIdMap(res)
dispatch('attach', { action: 'saved', value: attachments.size })
},
{
lookup: {
file: core.class.Blob
}
}
)
}
@@ -14,9 +14,9 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import { getBlobHref } from '@hcengineering/presentation'
import type { WithLookup } from '@hcengineering/core'
import { getFileUrl } from '@hcengineering/presentation'
import AttachmentPresenter from './AttachmentPresenter.svelte'
export let value: WithLookup<Attachment>
@@ -58,9 +58,7 @@
</script>
<video controls width={dimensions.width} height={dimensions.height} preload={preload ? 'auto' : 'none'}>
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then href}
<source src={href} />
{/await}
<source src={getFileUrl(value.file, value.name)} />
<track kind="captions" label={value.name} />
<div class="container">
<AttachmentPresenter {value} />
@@ -15,7 +15,7 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { getBlobHref, getClient } from '@hcengineering/presentation'
import { getClient, getFileUrl } from '@hcengineering/presentation'
import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui'
import { AttachmentGalleryPresenter } from '..'
import FileDownload from './icons/FileDownload.svelte'
@@ -56,12 +56,11 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<AttachmentGalleryPresenter value={attachment}>
<svelte:fragment slot="rowMenu">
{@const href = getFileUrl(attachment.file, attachment.name)}
<div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
<a {href} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
{/await}
<a {href} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
<IconMoreV size={'small'} />
</div>
@@ -15,7 +15,7 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { getBlobHref, getClient } from '@hcengineering/presentation'
import { getClient, getFileUrl } from '@hcengineering/presentation'
import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui'
import { AttachmentPresenter } from '..'
import FileDownload from './icons/FileDownload.svelte'
@@ -51,16 +51,15 @@
<div class="flex-col">
{#each attachments as attachment, i}
{@const href = getFileUrl(attachment.file, attachment.name)}
<div class="flex-between attachmentRow" class:fixed={i === selectedFileNumber}>
<div class="item flex">
<AttachmentPresenter value={attachment} />
</div>
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
<a {href} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
{/await}
<a {href} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
@@ -14,11 +14,11 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import { getBlobHref, getFileUrl } from '@hcengineering/presentation'
import { CircleButton, Progress } from '@hcengineering/ui'
import Play from './icons/Play.svelte'
import Pause from './icons/Pause.svelte'
import type { WithLookup } from '@hcengineering/core'
import { getFileUrl } from '@hcengineering/presentation'
import { CircleButton, Progress } from '@hcengineering/ui'
import Pause from './icons/Pause.svelte'
import Play from './icons/Play.svelte'
export let value: WithLookup<Attachment>
export let fullSize = false
@@ -48,9 +48,7 @@
</div>
</div>
<audio bind:duration bind:currentTime={time} bind:paused>
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then href}
<source src={href} type={value.type} />
{/await}
<source src={getFileUrl(value.file, value.name)} type={value.type} />
</audio>
<style lang="scss">
@@ -92,10 +92,7 @@
{ ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery },
{
sort: sortModeToOptionObject(selectedSort_),
limit: 200,
lookup: {
file: core.class.Blob
}
limit: 200
}
)
isLoading = false
@@ -16,7 +16,7 @@
// import { Doc } from '@hcengineering/core'
import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core'
import presentation, { ActionContext, IconDownload, getBlobHref, getBlobRef } from '@hcengineering/presentation'
import presentation, { ActionContext, getFileUrl, IconDownload } from '@hcengineering/presentation'
import { Button, Dialog } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import { getType } from '../utils'
@@ -40,7 +40,7 @@
})
let download: HTMLAnchorElement
$: type = getType(value.type)
$: srcRef = getBlobHref(value.$lookup?.file, value.file, value.name)
$: srcRef = getFileUrl(value.file, value.name)
</script>
<ActionContext context={{ mode: 'browser' }} />
@@ -17,14 +17,7 @@
import { Photo } from '@hcengineering/attachment'
import { Class, Doc, Ref, Space, type WithLookup } from '@hcengineering/core'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
FilePreviewPopup,
createQuery,
getBlobHref,
getClient,
uploadFile,
getBlobRef
} from '@hcengineering/presentation'
import { FilePreviewPopup, createQuery, getBlobRef, getClient, uploadFile } from '@hcengineering/presentation'
import { Button, IconAdd, Label, Spinner, showPopup } from '@hcengineering/ui'
import attachment from '../plugin'
import UploadDuo from './icons/UploadDuo.svelte'
@@ -99,7 +92,7 @@
if (item !== undefined) {
showPopup(
FilePreviewPopup,
{ file: item.$lookup?.file ?? item.file, name: item.name },
{ file: item.file, name: item.name, contentType: item.type },
item.type.startsWith('image/') ? 'centered' : 'float'
)
} else {
@@ -157,7 +150,7 @@
click(ev, image)
}}
>
{#await getBlobRef(image.$lookup?.file, image.file, image.name) then blobRef}
{#await getBlobRef(image.file, image.name) then blobRef}
<img src={blobRef.src} srcset={blobRef.srcset} alt={image.name} />
{/await}
</div>
@@ -25,11 +25,10 @@ import {
type Space,
type Timestamp
} from '@hcengineering/core'
import { derived, get, type Readable, writable } from 'svelte/store'
import { type ActivityMessage } from '@hcengineering/activity'
import activity, { type ActivityMessage, type ActivityReference } from '@hcengineering/activity'
import attachment from '@hcengineering/attachment'
import { combineActivityMessages } from '@hcengineering/activity-resources'
import { combineActivityMessages, sortActivityMessages } from '@hcengineering/activity-resources'
import { type ChatMessage } from '@hcengineering/chunter'
import notification, { type DocNotifyContext } from '@hcengineering/notification'
@@ -70,6 +69,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
private readonly metadataQuery = createQuery(true)
private readonly tailQuery = createQuery(true)
private readonly refsQuery = createQuery(true)
private chatId: Ref<Doc> | undefined = undefined
private readonly msgClass: Ref<Class<ActivityMessage>>
@@ -79,12 +79,14 @@ export class ChannelDataProvider implements IChannelDataProvider {
public readonly metadataStore = writable<MessageMetadata[]>([])
private readonly tailStore = writable<ActivityMessage[]>([])
private readonly chunksStore = writable<Chunk[]>([])
public readonly refsStore = writable<ActivityReference[]>([])
private readonly isInitialLoadingStore = writable(false)
private readonly isInitialLoadedStore = writable(false)
private readonly isTailLoading = writable(false)
readonly isTailLoaded = writable(false)
readonly isRefsLoading = writable(false)
public datesStore = writable<Timestamp[]>([])
public newTimestampStore = writable<Timestamp | undefined>(undefined)
@@ -92,8 +94,8 @@ export class ChannelDataProvider implements IChannelDataProvider {
public isLoadingMoreStore = writable(false)
public isLoadingStore = derived(
[this.isInitialLoadedStore, this.isTailLoading],
([initialLoaded, tailLoading]) => !initialLoaded || tailLoading
[this.isInitialLoadedStore, this.isTailLoading, this.isRefsLoading],
([initialLoaded, tailLoading, isRefsLoading]) => !initialLoaded || tailLoading || isRefsLoading
)
private readonly backwardNextStore = writable<Chunk | undefined>(undefined)
@@ -107,8 +109,9 @@ export class ChannelDataProvider implements IChannelDataProvider {
private nextChunkAdding = false
public messagesStore = derived([this.chunksStore, this.tailStore], ([chunks, tail]) => {
return [...chunks.map(({ data }) => data).flat(), ...tail]
public messagesStore = derived([this.chunksStore, this.tailStore, this.refsStore], ([chunks, tail, refs]) => {
const data = chunks.map(({ data, to, from }) => mergeWithRefs(data, refs, from, to))
return [...data.flat(), ...mergeWithRefs(tail, refs, tail[0]?.createdOn)]
})
public canLoadNextForwardStore = derived([this.messagesStore, this.forwardNextStore], ([messages, forwardNext]) => {
@@ -123,18 +126,20 @@ export class ChannelDataProvider implements IChannelDataProvider {
chatId: Ref<Doc>,
_class: Ref<Class<ActivityMessage>>,
selectedMsgId: Ref<ActivityMessage> | undefined,
loadAll = false
loadAll = false,
withRefs = false
) {
this.chatId = chatId
this.msgClass = _class
this.selectedMsgId = selectedMsgId
void this.loadData(loadAll)
void this.loadData(loadAll, withRefs)
}
public destroy (): void {
this.clearData()
this.metadataQuery.unsubscribe()
this.tailQuery.unsubscribe()
this.refsQuery.unsubscribe()
}
public canLoadMore (mode: LoadMode, timestamp?: Timestamp): boolean {
@@ -168,11 +173,29 @@ export class ChannelDataProvider implements IChannelDataProvider {
this.clearMessages()
}
private async loadData (loadAll = false): Promise<void> {
loadRefs (): void {
// Load references from other spaces separately because they can have any different spaces
this.refsQuery.query(
activity.class.ActivityReference,
{ attachedTo: this.chatId, space: { $ne: this.space } },
(res) => {
this.refsStore.set(res)
this.isRefsLoading.set(false)
},
{ sort: { createdOn: SortingOrder.Ascending } }
)
}
private async loadData (loadAll = false, withRefs = false): Promise<void> {
if (this.chatId === undefined) {
return
}
if (withRefs && this.msgClass === activity.class.ActivityMessage) {
this.isRefsLoading.set(true)
this.loadRefs()
}
this.metadataQuery.query(
this.msgClass,
{ attachedTo: this.chatId, space: this.space },
@@ -622,3 +645,21 @@ export class ChannelDataProvider implements IChannelDataProvider {
return true
}
}
function mergeWithRefs (
messages: ActivityMessage[],
refs: ActivityReference[],
from?: Timestamp,
to?: Timestamp
): ActivityMessage[] {
if (from === undefined) return messages
if (refs.length === 0) return messages
const refsFiltered = refs.filter(
({ createdOn }) => (createdOn ?? 0) >= from && (to === undefined || (createdOn ?? 0) <= to)
)
if (refsFiltered.length === 0) return messages
return sortActivityMessages(messages.concat(refsFiltered))
}
@@ -15,7 +15,7 @@
<script lang="ts">
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import notification, { DocNotifyContext } from '@hcengineering/notification'
import activity, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
import activity, { ActivityMessage, ActivityMessagesFilter, WithReferences } from '@hcengineering/activity'
import { getClient, isSpace } from '@hcengineering/presentation'
import { getMessageFromLoc, messageInFocus } from '@hcengineering/activity-resources'
import { location as locationStore } from '@hcengineering/ui'
@@ -57,6 +57,8 @@
dataProvider = undefined
})
let refsLoaded = false
$: isDocChannel = !hierarchy.isDerived(object._class, chunter.class.ChunterSpace)
$: _class = isDocChannel ? activity.class.ActivityMessage : chunter.class.ChatMessage
$: collection = isDocChannel ? 'comments' : 'messages'
@@ -78,11 +80,17 @@
attachedTo: object._id,
user: getCurrentAccount()._id
}))
const hasRefs = ((object as WithReferences<Doc>).references ?? 0) > 0
refsLoaded = hasRefs
const space = isSpace(object) ? object._id : object.space
dataProvider = new ChannelDataProvider(ctx, space, attachedTo, _class, selectedMessageId, loadAll)
dataProvider = new ChannelDataProvider(ctx, space, attachedTo, _class, selectedMessageId, loadAll, hasRefs)
}
}
$: if (dataProvider && !refsLoaded && ((object as WithReferences<Doc>).references ?? 0) > 0) {
dataProvider.loadRefs()
refsLoaded = true
}
</script>
{#if dataProvider}
@@ -17,7 +17,7 @@
import { getDocTitle } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import { Channel } from '@hcengineering/chunter'
import { ActivityMessagesFilter } from '@hcengineering/activity'
import { ActivityMessagesFilter, WithReferences } from '@hcengineering/activity'
import contact from '@hcengineering/contact'
import Header from './Header.svelte'
@@ -27,7 +27,7 @@
export let _id: Ref<Doc>
export let _class: Ref<Class<Doc>>
export let object: Doc | undefined
export let object: WithReferences<Doc> | undefined
export let allowClose: boolean = false
export let canOpen: boolean = false
export let withAside: boolean = false
@@ -78,6 +78,6 @@
on:close
>
{#if object}
<PinnedMessages {_id} {_class} space={object.space} on:select />
<PinnedMessages {_id} {_class} space={object.space} withRefs={(object.references ?? 0) > 0} on:select />
{/if}
</Header>
@@ -646,10 +646,7 @@
return
}
const lastMetadata = metadata[metadata.length - 1]
const lastMessage = displayMessages[displayMessages.length - 1]
if (lastMetadata._id !== lastMessage._id) {
if (!$isTailLoadedStore) {
showScrollDownButton = true
} else if (element != null) {
const { scrollHeight, scrollTop, offsetHeight } = element
@@ -17,8 +17,8 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources'
import { ChunterSpace } from '@hcengineering/chunter'
import core, { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation'
import { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation'
import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui'
export let channel: ChunterSpace | undefined
@@ -68,10 +68,7 @@
{
limit: ATTACHEMNTS_LIMIT,
sort,
total: true,
lookup: {
file: core.class.Blob
}
total: true
}
)
</script>
@@ -86,11 +83,9 @@
<AttachmentPresenter value={attachment} />
</div>
<div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then blobRef}
<a href={blobRef} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
{/await}
<a href={getFileUrl(attachment.file, attachment.name)} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showMenu(event, attachment, i)}>
@@ -28,13 +28,16 @@
export let space: Ref<Space>
export let _class: Ref<Class<Doc>>
export let _id: Ref<Doc>
export let withRefs = false
const dispatch = createEventDispatcher()
const pinnedQuery = createQuery()
const pinnedThreadsQuery = createQuery()
const pinnedRefsQuery = createQuery()
let pinnedMessagesCount = 0
let pinnedThreadsCount = 0
let refsCount = 0
$: channelSpace = getChannelSpace(_class, _id, space)
$: pinnedQuery.query(
@@ -55,10 +58,21 @@
{ projection: { _id: 1, space: 1, objectId: 1, isPinned: 1 } }
)
$: if (withRefs) {
pinnedRefsQuery.query(
activity.class.ActivityReference,
{ attachedTo: _id, isPinned: true, space: { $ne: channelSpace } },
(res) => {
refsCount = res.total
},
{ limit: 1, total: true }
)
}
function openMessagesPopup (ev: MouseEvent): void {
showPopup(
PinnedMessagesPopup,
{ attachedTo: _id, attachedToClass: _class, space: channelSpace },
{ attachedTo: _id, attachedToClass: _class, space: channelSpace, withRefs },
eventToHTMLElement(ev),
(result) => {
if (result == null) return
@@ -67,7 +81,7 @@
)
}
$: count = pinnedMessagesCount + pinnedThreadsCount
$: count = pinnedMessagesCount + pinnedThreadsCount + refsCount
</script>
{#if count > 0}
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { createQuery, getClient } from '@hcengineering/presentation'
import activity, { ActivityMessage } from '@hcengineering/activity'
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
import { ActivityMessagePresenter, sortActivityMessages } from '@hcengineering/activity-resources'
import { ActionIcon, IconClose } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
@@ -26,14 +26,17 @@
export let attachedTo: Ref<Doc>
export let attachedToClass: Ref<Class<Doc>>
export let space: Ref<Space>
export let withRefs = false
const client = getClient()
const dispatch = createEventDispatcher()
const pinnedQuery = createQuery()
const pinnedThreadsQuery = createQuery()
const pinnedRefsQuery = createQuery()
let pinnedMessages: ActivityMessage[] = []
let pinnedThreads: ThreadMessage[] = []
let pinnedRefs: ActivityReference[] = []
$: pinnedQuery.query(
activity.class.ActivityMessage,
@@ -51,6 +54,16 @@
}
)
$: if (withRefs) {
pinnedRefsQuery.query(
activity.class.ActivityReference,
{ attachedTo, isPinned: true, space: { $ne: space } },
(res) => {
pinnedRefs = res
}
)
}
$: if (pinnedMessages.length === 0 && pinnedThreads.length === 0) {
dispatch('close', undefined)
}
@@ -59,7 +72,10 @@
await client.update(message, { isPinned: false })
}
$: displayMessages = sortActivityMessages(pinnedMessages.concat(pinnedThreads), SortingOrder.Descending)
$: displayMessages = sortActivityMessages(
pinnedMessages.concat(pinnedThreads).concat(pinnedRefs),
SortingOrder.Descending
)
</script>
<div class="antiPopup vScroll popup">
@@ -15,7 +15,7 @@
<script lang="ts">
import { Person, PersonAccount } from '@hcengineering/contact'
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { Class, Doc, getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import { Class, Doc, getCurrentAccount, Ref, Space, WithLookup } from '@hcengineering/core'
import { getClient, MessageViewer } from '@hcengineering/presentation'
import { AttachmentDocList, AttachmentImageSize } from '@hcengineering/attachment-resources'
import { getDocLinkTitle } from '@hcengineering/view-resources'
@@ -80,7 +80,7 @@
$: person = account?.person !== undefined ? $personByIdStore.get(account.person) : undefined
$: value !== undefined &&
getParentMessage(value.attachedToClass, value.attachedTo).then((res) => {
getParentMessage(value.attachedToClass, value.attachedTo, value.space).then((res) => {
parentMessage = res as DisplayActivityMessage
})
@@ -107,9 +107,13 @@
stale = false
}
async function getParentMessage (_class: Ref<Class<Doc>>, _id: Ref<Doc>): Promise<ActivityMessage | undefined> {
async function getParentMessage (
_class: Ref<Class<Doc>>,
_id: Ref<Doc>,
space: Ref<Space>
): Promise<ActivityMessage | undefined> {
if (hierarchy.isDerived(_class, activity.class.ActivityMessage)) {
return await client.findOne<ActivityMessage>(_class, { _id: _id as Ref<ActivityMessage> })
return await client.findOne<ActivityMessage>(_class, { _id: _id as Ref<ActivityMessage>, space })
}
}
@@ -41,11 +41,6 @@
},
(res) => {
attachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)
} else {
@@ -27,79 +27,67 @@
import ThreadMessagePreview from '../threads/ThreadMessagePreview.svelte'
export let context: DocNotifyContext
export let object: ChatMessage
const client = getClient()
const hierarchy = client.getHierarchy()
let title: string | undefined = undefined
let parentMessage: ChatMessage | undefined = undefined
let object: Doc | undefined = undefined
let channel: Doc | undefined = undefined
$: isThread = hierarchy.isDerived(context.attachedToClass, chunter.class.ThreadMessage)
$: void client
.findOne(context.attachedToClass as Ref<Class<ChatMessage>>, { _id: context.attachedTo as Ref<ChatMessage> })
.then((res) => {
parentMessage = res
})
$: loadObject(parentMessage, isThread)
$: object &&
getDocLinkTitle(client, object._id, object._class, object).then((res) => {
$: loadChannel(object, isThread)
$: channel &&
getDocLinkTitle(client, channel._id, channel._class, channel).then((res) => {
title = res
})
function loadObject (parentMessage: ChatMessage | undefined, isThread: boolean): void {
if (parentMessage === undefined) {
object = undefined
return
}
const _class = isThread ? (parentMessage as ThreadMessage).objectClass : parentMessage.attachedToClass
const _id = isThread ? (parentMessage as ThreadMessage).objectId : parentMessage.attachedTo
void client.findOne(_class, { _id }).then((res) => {
object = res
function loadChannel (object: ChatMessage, isThread: boolean): void {
const _class = isThread ? (object as ThreadMessage).objectClass : object.attachedToClass
const _id = isThread ? (object as ThreadMessage).objectId : object.attachedTo
console.log({ _class, _id, isThread, object })
void client.findOne(_class, { _id, ...(isThread ? { space: object.space } : {}) }).then((res) => {
channel = res
})
}
function toThread (message: ChatMessage): ThreadMessage {
return message as ThreadMessage
}
function isAvatarIcon (_class: Ref<Class<Doc>>): boolean {
return hierarchy.isDerived(_class, contact.class.Person) || hierarchy.isDerived(_class, chunter.class.DirectMessage)
}
</script>
{#if parentMessage}
<span class="flex-presenter flex-gap-1 font-semi-bold">
{#if isThread || (parentMessage.replies ?? 0) > 0}
<Label label={chunter.string.Thread} />
{:else}
<Label label={chunter.string.Message} />
{/if}
{#if title && object}
<span class="lower">
<Label label={chunter.string.In} />
</span>
{#await getDocTitle(client, object._id, object._class, object) then tooltipLabel}
<span
class="flex-presenter flex-gap-0-5"
use:tooltip={tooltipLabel ? { label: getEmbeddedLabel(tooltipLabel) } : undefined}
>
<ObjectIcon
value={object}
size={hierarchy.isDerived(object._class, contact.class.Person) ? 'tiny' : 'small'}
/>
<span class="overflow-label">
{title}
</span>
<span class="flex-presenter flex-gap-1 font-semi-bold">
{#if isThread || (object.replies ?? 0) > 0}
<Label label={chunter.string.Thread} />
{:else}
<Label label={chunter.string.Message} />
{/if}
{#if title && channel}
<span class="lower">
<Label label={chunter.string.In} />
</span>
{#await getDocTitle(client, channel._id, channel._class, channel) then tooltipLabel}
<span
class="flex-presenter flex-gap-0-5"
use:tooltip={tooltipLabel ? { label: getEmbeddedLabel(tooltipLabel) } : undefined}
>
<ObjectIcon value={channel} size={isAvatarIcon(channel._class) ? 'tiny' : 'small'} />
<span class="overflow-label">
{title}
</span>
{/await}
{/if}
</span>
<span class="font-normal">
{#if isThread}
<ThreadMessagePreview value={toThread(parentMessage)} readonly type="content-only" />
{:else}
<ChatMessagePreview value={parentMessage} readonly type="content-only" />
{/if}
</span>
{/if}
</span>
{/await}
{/if}
</span>
<span class="font-normal">
{#if isThread}
<ThreadMessagePreview value={toThread(object)} readonly type="content-only" />
{:else}
<ChatMessagePreview value={object} readonly type="content-only" />
{/if}
</span>
@@ -38,7 +38,7 @@
$: if (empValue === undefined) {
void getClient()
.findOne(contact.class.Contact, { _id }, { lookup: { avatar: core.class.Blob } })
.findOne(contact.class.Contact, { _id })
.then((c) => {
_contact = c
})
+5 -5
View File
@@ -15,11 +15,11 @@
//
import {
type Channel,
type AvatarInfo,
type Contact,
getGravatarUrl,
getName,
type AvatarInfo,
type Channel,
type Contact,
type Person,
type PersonAccount
} from '@hcengineering/contact'
@@ -49,6 +49,7 @@ import {
type ColorDefinition,
type TooltipAlignment
} from '@hcengineering/ui'
import { AggregationManager } from '@hcengineering/view-resources'
import AccountArrayEditor from './components/AccountArrayEditor.svelte'
import AccountBox from './components/AccountBox.svelte'
import AssigneeBox from './components/AssigneeBox.svelte'
@@ -122,7 +123,6 @@ import NameChangedActivityMessage from './components/activity/NameChangedActivit
import IconAddMember from './components/icons/AddMember.svelte'
import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
import IconMembers from './components/icons/Members.svelte'
import { AggregationManager } from '@hcengineering/view-resources'
import { get, writable } from 'svelte/store'
import contact from './plugin'
@@ -408,7 +408,7 @@ export default async (): Promise<Resources> => ({
color: getPersonColor(person, name)
}
}
const blobRef = await getBlobRef(person.$lookup?.avatar, person.avatar, undefined, width)
const blobRef = await getBlobRef(person.avatar, undefined, width)
return {
url: blobRef.src,
srcSet: blobRef.srcset,
+4 -18
View File
@@ -320,19 +320,10 @@ function fillStores (): void {
const accountPersonQuery = createQuery(true)
const query = createQuery(true)
query.query(
contact.mixin.Employee,
{},
(res) => {
employeesStore.set(res)
employeeByIdStore.set(toIdMap(res))
},
{
lookup: {
avatar: core.class.Blob
}
}
)
query.query(contact.mixin.Employee, {}, (res) => {
employeesStore.set(res)
employeeByIdStore.set(toIdMap(res))
})
const accountQ = createQuery(true)
accountQ.query(contact.class.PersonAccount, {}, (res) => {
@@ -345,11 +336,6 @@ function fillStores (): void {
{ _id: { $in: persons }, [contact.mixin.Employee]: { $exists: false } },
(res) => {
personAccountPersonByIdStore.set(toIdMap(res))
},
{
lookup: {
avatar: core.class.Blob
}
}
)
})
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import core, { type Blob, type WithLookup } from '@hcengineering/core'
import { type Blob, type Ref, type WithLookup } from '@hcengineering/core'
import drive, { type File, type FileVersion } from '@hcengineering/drive'
import { FilePreview, createQuery } from '@hcengineering/presentation'
@@ -26,32 +26,23 @@
const dispatch = createEventDispatcher()
const query = createQuery()
let blob: Blob | undefined = undefined
let blob: Ref<Blob> | undefined = undefined
let version: WithLookup<FileVersion> | undefined = undefined
let contentType: string | undefined
$: query.query(
drive.class.FileVersion,
{ _id: object.file },
(res) => {
;[version] = res
blob = version?.$lookup?.file
},
{
lookup: {
file: core.class.Blob
}
}
)
$: query.query(drive.class.FileVersion, { _id: object.file }, (res) => {
;[version] = res
blob = version.file
contentType = version.type
})
onMount(() => {
dispatch('open', { ignoreKeys: ['parent', 'path', 'version', 'versions'] })
})
</script>
{#if object !== undefined && version !== undefined}
{#if blob !== undefined}
<FilePreview file={blob} name={version.name} metadata={version.metadata} fit />
{/if}
{#if object !== undefined && version !== undefined && blob !== undefined && contentType !== undefined}
<FilePreview file={blob} {contentType} name={version.name} metadata={version.metadata} fit />
{#if object.versions > 1}
<div class="w-full mt-6">
@@ -24,7 +24,6 @@
export let readonly: boolean = false
const options: FindOptions<FileVersion> = {
lookup: { file: core.class.Blob },
sort: { version: SortingOrder.Descending }
}
</script>
@@ -24,9 +24,5 @@
<Scroller>
<DocAttributeBar {object} {readonly} ignoreKeys={[]} />
{#if object.$lookup?.file}
<DocAttributeBar object={object.$lookup.file} {readonly} ignoreKeys={['name', 'file', 'version', 'version']} />
{/if}
<div class="space-divider bottom" />
</Scroller>
@@ -16,7 +16,7 @@
import { type Ref, type WithLookup } from '@hcengineering/core'
import { createFileVersion, type File as DriveFile, type FileVersion } from '@hcengineering/drive'
import { Panel } from '@hcengineering/panel'
import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation'
import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation'
import { Button, IconMoreH } from '@hcengineering/ui'
import { showFilesUploadPopup } from '@hcengineering/uploader'
import view from '@hcengineering/view'
@@ -108,17 +108,15 @@
</svelte:fragment>
<svelte:fragment slot="utils">
{#await getBlobHref(undefined, version.file, object.name) then href}
<a class="no-line" {href} download={object.name} bind:this={download}>
<Button
icon={IconDownload}
iconProps={{ size: 'medium' }}
kind={'icon'}
showTooltip={{ label: drive.string.Download }}
on:click={handleDownloadFile}
/>
</a>
{/await}
<a class="no-line" href={getFileUrl(version.file, object.name)} download={object.name} bind:this={download}>
<Button
icon={IconDownload}
iconProps={{ size: 'medium' }}
kind={'icon'}
showTooltip={{ label: drive.string.Download }}
on:click={handleDownloadFile}
/>
</a>
<Button
icon={IconUpload}
iconProps={{ size: 'medium' }}
@@ -38,17 +38,11 @@
return
}
if (value.$lookup?.file === undefined) {
return
}
const blob = value.$lookup?.file
showPopup(
FilePreviewPopup,
{
file: blob._id,
contentType: blob.contentType,
file: value.file,
contentType: value.type,
name: value.name,
metadata: value.metadata
},
@@ -44,7 +44,7 @@
{#if isFolder}
<Icon icon={IconFolderThumbnail} size={'full'} fill={'var(--global-no-priority-PriorityColor)'} />
{:else if previewRef != null && isImage && !isError}
{#await getBlobRef(undefined, previewRef, object.name, sizeToWidth(size)) then blobSrc}
{#await getBlobRef(previewRef, object.name, sizeToWidth(size)) then blobSrc}
<img
draggable="false"
class="img-fit"
+3 -3
View File
@@ -16,7 +16,6 @@
import { type Doc, type Ref, type WithLookup } from '@hcengineering/core'
import drive, { type Drive, type File, type FileVersion, type Folder } from '@hcengineering/drive'
import { type Resources } from '@hcengineering/platform'
import { getBlobHref } from '@hcengineering/presentation'
import { showPopup, type Location } from '@hcengineering/ui'
import CreateDrive from './components/CreateDrive.svelte'
@@ -37,8 +36,9 @@ import GridView from './components/GridView.svelte'
import MoveResource from './components/MoveResource.svelte'
import ResourcePresenter from './components/ResourcePresenter.svelte'
import { getFileUrl } from '@hcengineering/presentation'
import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation'
import { showCreateFolderPopup, showRenameResourcePopup, restoreFileVersion } from './utils'
import { restoreFileVersion, showCreateFolderPopup, showRenameResourcePopup } from './utils'
async function CreateRootFolder (doc: Drive): Promise<void> {
await showCreateFolderPopup(doc._id, drive.ids.Root)
@@ -57,7 +57,7 @@ async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): P
for (const file of files) {
const version = file.$lookup?.file
if (version != null) {
const href = await getBlobHref(undefined, version.file, version.name)
const href = getFileUrl(version.file, version.name)
const link = document.createElement('a')
link.style.display = 'none'
link.target = '_blank'
@@ -52,11 +52,6 @@
},
(res) => {
attachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)
@@ -36,11 +36,6 @@
},
(res) => {
attachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)
@@ -20,10 +20,10 @@
DocNotifyContext,
InboxNotification
} from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import { createQuery, getClient, isSpace, isSpaceClass } from '@hcengineering/presentation'
import { getDocTitle, getDocIdentifier, Menu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { Class, Doc, IdMap, Ref, WithLookup } from '@hcengineering/core'
import core, { Class, Doc, IdMap, Ref, WithLookup } from '@hcengineering/core'
import chunter from '@hcengineering/chunter'
import { personAccountByIdStore } from '@hcengineering/contact-resources'
import { Person, PersonAccount } from '@hcengineering/contact'
@@ -47,6 +47,22 @@
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
const query = createQuery()
let object: Doc | undefined = undefined
$: query.query(
value.attachedToClass,
{ _id: value.attachedTo, space: isSpaceClass(value.attachedToClass) ? core.space.Space : value.space },
(res) => {
object = res[0]
},
{ limit: 1 }
)
$: if (object?._id !== value.attachedTo) {
object = undefined
}
let isActionMenuOpened = false
let unreadCount = 0
@@ -56,13 +72,15 @@
let idTitle: string | undefined
let title: string | undefined
$: void getDocIdentifier(client, value.attachedTo, value.attachedToClass).then((res) => {
idTitle = res
})
$: object &&
getDocIdentifier(client, object._id, object._class, object).then((res) => {
idTitle = res
})
$: void getDocTitle(client, value.attachedTo, value.attachedToClass).then((res) => {
title = res
})
$: object &&
getDocTitle(client, object._id, object._class, object).then((res) => {
title = res
})
$: presenterMixin = hierarchy.classHierarchyMixin(
value.attachedToClass,
@@ -184,73 +202,75 @@
dispatch('click', { context: value })
}}
>
<div class="header">
<NotifyContextIcon {value} notifyCount={unreadCount} />
{#if object}
<div class="header">
<NotifyContextIcon {value} notifyCount={unreadCount} {object} />
<div class="labels">
{#if presenterMixin?.labelPresenter}
<Component is={presenterMixin.labelPresenter} props={{ context: value }} />
{:else}
{#if idTitle}
{idTitle}
<div class="labels">
{#if presenterMixin?.labelPresenter}
<Component is={presenterMixin.labelPresenter} props={{ context: value, object }} />
{:else}
<Label label={hierarchy.getClass(value.attachedToClass).label} />
{/if}
<span class="title overflow-label clear-mins" {title}>
{#if title}
{title}
{#if idTitle}
{idTitle}
{:else}
<Label label={hierarchy.getClass(value.attachedToClass).label} />
{/if}
</span>
{/if}
</div>
<div class="actions clear-mins">
<div class="flex-center">
{#if archivingPromise !== undefined}
<Spinner size="small" />
{:else}
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
<span class="title overflow-label clear-mins" {title}>
{#if title}
{title}
{:else}
<Label label={hierarchy.getClass(value.attachedToClass).label} />
{/if}
</span>
{/if}
</div>
<ButtonIcon
icon={IconMoreV}
size="small"
kind="tertiary"
inheritColor
pressed={isActionMenuOpened}
on:click={showMenu}
/>
</div>
</div>
<div class="content">
<div class="notifications">
{#each groupedNotifications.slice(0, maxNotifications) as group (getKey(group))}
<div class="notification">
<!-- use:tooltip={canShowTooltip(group)-->
<!-- ? {-->
<!-- component: MessagesPopup,-->
<!-- props: { context: value, notifications: group }-->
<!-- }-->
<!-- : undefined}-->
<div class="embeddedMarker" />
<InboxNotificationPresenter
value={group[0]}
{viewlets}
space={value.space}
on:click={(e) => {
e.preventDefault()
e.stopPropagation()
dispatch('click', { context: value, notification: group[0] })
}}
/>
<div class="actions clear-mins">
<div class="flex-center">
{#if archivingPromise !== undefined}
<Spinner size="small" />
{:else}
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
{/if}
</div>
{/each}
<ButtonIcon
icon={IconMoreV}
size="small"
kind="tertiary"
inheritColor
pressed={isActionMenuOpened}
on:click={showMenu}
/>
</div>
</div>
</div>
<div class="content">
<div class="notifications">
{#each groupedNotifications.slice(0, maxNotifications) as group (getKey(group))}
<div class="notification">
<!-- use:tooltip={canShowTooltip(group)-->
<!-- ? {-->
<!-- component: MessagesPopup,-->
<!-- props: { context: value, notifications: group }-->
<!-- }-->
<!-- : undefined}-->
<div class="embeddedMarker" />
<InboxNotificationPresenter
value={group[0]}
{object}
{viewlets}
space={value.space}
on:click={(e) => {
e.preventDefault()
e.stopPropagation()
dispatch('click', { context: value, notification: group[0] })
}}
/>
</div>
{/each}
</div>
</div>
{/if}
</div>
<style lang="scss">
@@ -261,6 +281,7 @@
cursor: pointer;
padding: var(--spacing-1_5) var(--spacing-1);
border-bottom: 1px solid var(--global-ui-BorderColor);
min-height: 5.625rem;
.header {
position: relative;
@@ -14,9 +14,9 @@
-->
<script lang="ts">
import { DocNotifyContext } from '@hcengineering/notification'
import { Doc } from '@hcengineering/core'
import core, { Doc } from '@hcengineering/core'
import { getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, getClient, isSpaceClass } from '@hcengineering/presentation'
import chunter from '@hcengineering/chunter'
import NotifyContextIcon from './NotifyContextIcon.svelte'
@@ -27,9 +27,13 @@
let object: Doc | undefined
$: objectQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
object = res[0]
})
$: objectQuery.query(
value.attachedToClass,
{ _id: value.attachedTo, space: isSpaceClass(value.attachedToClass) ? core.space.Space : value.space },
(res) => {
object = res[0]
}
)
async function getTitle (object: Doc) {
if (object._class === chunter.class.DirectMessage) {
@@ -41,7 +45,7 @@
{#if object}
<div class="flex-presenter">
<NotifyContextIcon {value} size="small" />
<NotifyContextIcon {value} {object} size="small" />
<div class="mr-4" />
{#await getTitle(object) then title}
@@ -15,7 +15,7 @@
<script lang="ts">
import notification, { DocNotifyContext } from '@hcengineering/notification'
import { Component, Icon, IconSize } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getClient } from '@hcengineering/presentation'
import { classIcon } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import { Doc } from '@hcengineering/core'
@@ -25,21 +25,12 @@
export let value: DocNotifyContext
export let size: IconSize = 'medium'
export let notifyCount: number = 0
export let object: Doc | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
let object: Doc | undefined = undefined
$: if (object?._id !== value.attachedTo) {
object = undefined
}
$: iconMixin = hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectIcon)
$: iconMixin &&
query.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
object = res[0]
})
</script>
<div class="container">
@@ -26,6 +26,7 @@
$: void client
.findAll(activity.class.Reaction, {
space: message.space,
_id: { $in: [message.objectId, ...(message?.previousMessages?.map((a) => a.objectId) ?? [])] as Ref<Reaction>[] }
})
.then((res) => {
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { Ref, Space, matchQuery } from '@hcengineering/core'
import { Ref, Space, matchQuery, Doc } from '@hcengineering/core'
import notification, {
ActivityInboxNotification,
ActivityNotificationViewlet,
@@ -30,6 +30,7 @@
import { getActions } from '@hcengineering/view-resources'
import { getResource } from '@hcengineering/platform'
export let object: Doc | undefined
export let value: DisplayActivityInboxNotification
export let viewlets: ActivityNotificationViewlet[] = []
export let space: Ref<Space> | undefined = undefined
@@ -100,6 +101,6 @@
on:click
/>
{:else}
<ActivityMessagePreview value={displayMessage} {actions} {space} on:click />
<ActivityMessagePreview value={displayMessage} {actions} {space} doc={object} on:click />
{/if}
{/if}
@@ -20,6 +20,7 @@
import { ActivityNotificationViewlet, DisplayInboxNotification } from '@hcengineering/notification'
export let value: DisplayInboxNotification
export let object: Doc | undefined
export let viewlets: ActivityNotificationViewlet[] = []
export let space: Ref<Space> | undefined = undefined
@@ -30,5 +31,5 @@
</script>
{#if objectPresenter}
<Component is={objectPresenter.presenter} props={{ value, viewlets, space }} on:click />
<Component is={objectPresenter.presenter} props={{ value, viewlets, space, object }} on:click />
{/if}
+7 -3
View File
@@ -76,9 +76,13 @@ const providerSettingsQuery = createQuery(true)
const typeSettingsQuery = createQuery(true)
export function loadNotificationSettings (): void {
providerSettingsQuery.query(notification.class.NotificationProviderSetting, {}, (res) => {
providersSettings.set(res)
})
providerSettingsQuery.query(
notification.class.NotificationProviderSetting,
{ space: core.space.Workspace },
(res) => {
providersSettings.set(res)
}
)
typeSettingsQuery.query(notification.class.NotificationTypeSetting, {}, (res) => {
typesSettings.set(res)
})
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import type { Product, ProductVersion } from '@hcengineering/products'
import { FindOptions, SortingOrder } from '@hcengineering/core'
import core, { FindOptions, SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Label, Loading } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
@@ -45,6 +45,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {
@@ -20,6 +20,7 @@
import { EmployeeBox, ExpandRightDouble, UserBox } from '@hcengineering/contact-resources'
import {
Account,
AccountRole,
Class,
Client,
Doc,
@@ -28,10 +29,9 @@
Ref,
SortingOrder,
Space,
Status as TaskStatus,
fillDefaults,
generateId,
Status as TaskStatus,
AccountRole,
getCurrentAccount,
hasAccountRole
} from '@hcengineering/core'
@@ -43,10 +43,10 @@
createQuery,
getClient
} from '@hcengineering/presentation'
import type { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import { recruitId, type Applicant, type Candidate, type Vacancy } from '@hcengineering/recruit'
import task, { TaskType, getStates, makeRank } from '@hcengineering/task'
import { TaskKindSelector, selectedTypeStore, typeStore } from '@hcengineering/task-resources'
import { EmptyMarkup } from '@hcengineering/text'
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
import ui, {
Button,
ColorPopup,
@@ -132,8 +132,11 @@
if (candidateInstance === undefined) {
throw new Error('contact not found')
}
const ops = client.apply(generateId(), recruitId + '.Create.CreateApplication')
if (!client.getHierarchy().hasMixin(candidateInstance, recruit.mixin.Candidate)) {
await client.createMixin<Contact, Candidate>(
await ops.createMixin<Contact, Candidate>(
candidateInstance._id,
candidateInstance._class,
candidateInstance.space,
@@ -144,7 +147,7 @@
const number = (incResult as any).object.sequence
await client.addCollection(
await ops.addCollection(
recruit.class.Applicant,
_space,
candidateInstance._id,
@@ -166,11 +169,12 @@
await descriptionBox.createAttachments()
if (_comment.trim().length > 0) {
await client.addCollection(chunter.class.ChatMessage, _space, doc._id, recruit.class.Applicant, 'comments', {
if (_comment.trim().length > 0 && !isEmptyMarkup(_comment)) {
await ops.addCollection(chunter.class.ChatMessage, _space, doc._id, recruit.class.Applicant, 'comments', {
message: _comment
})
}
await ops.commit()
}
async function invokeValidate (
@@ -366,7 +366,7 @@
const formattedSkills = (doc.skills.map((s) => s.toLowerCase()) ?? []).filter(
(skill) => !namedElements.has(skill)
)
const refactoredSkills = []
const refactoredSkills: any[] = []
if (formattedSkills.length > 0) {
const existingTags = Array.from(namedElements.keys()).filter((x) => x.length > 2)
const regex = /\S+(?:[-+]\S+)+/g
@@ -755,6 +755,7 @@
FilePreviewPopup,
{
file: object.resumeUuid,
contentType: object.resumeType,
name: object.resumeName
},
object.resumeType?.startsWith('image/') ? 'centered' : 'float'
@@ -51,6 +51,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {
+2 -1
View File
@@ -1,6 +1,7 @@
{
"string": {
"ContactUs": "Contact us",
"ReportBug": "Report a Bug?"
"ReportBug": "Report a Bug?",
"PrivacyPolicy": "Privacy Policy"
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"string": {
"ContactUs": "Contacta con Nosotros",
"ReportBug": "Reportar un error?"
"ReportBug": "Reportar un error?",
"PrivacyPolicy": "Política de Privacidad"
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"string": {
"ContactUs": "Contactez nous",
"ReportBug": "Rapporter un bug?"
"ReportBug": "Rapporter un bug?",
"PrivacyPolicy": "Politique de Confidentialité"
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"string": {
"ContactUs": "Contate-nos",
"ReportBug": "Reportar um erro?"
"ReportBug": "Reportar um erro?",
"PrivacyPolicy": "Política de Privacidade"
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"string": {
"ContactUs": "Связаться с нами",
"ReportBug": "Сообщить об ошибке?"
"ReportBug": "Сообщить об ошибке?",
"PrivacyPolicy": "Политика конфиденциальности"
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"string": {
"ContactUs": "联系我们",
"ReportBug": "报告错误?"
"ReportBug": "报告错误?",
"PrivacyPolicy": "隐私政策"
}
}
+3 -1
View File
@@ -24,6 +24,7 @@ export { deleteSupportConversation, updateSupportConversation } from './utils'
export const supportLink = 'https://join.slack.com/t/hulycommunity/shared_invite/zt-2cyrevz8g-AGqEDZNsujbn4wHOWd7myg'
export const reportBugLink = 'https://github.com/hcengineering/platform/issues/new'
export const docsLink = 'http://docs.huly.io/'
export const privacyPolicyLink = 'https://v1.huly.io/legal/privacy/'
/**
* @public
@@ -43,6 +44,7 @@ export default plugin(supportId, {
},
string: {
ContactUs: '' as IntlString,
ReportBug: '' as IntlString
ReportBug: '' as IntlString,
PrivacyPolicy: '' as IntlString
}
})
@@ -146,12 +146,15 @@
kind={'ghost'}
size={'large'}
icon={show ? IconView : IconViewHide}
dataId={`btn${show ? 'Collapse' : 'Expand'}`}
on:click={() => {
show = !show
}}
/>
{/if}
{#if !hideAdd}<Button kind={'ghost'} size={'large'} icon={IconAdd} on:click={createTagElementPopup} />{/if}
{#if !hideAdd}
<Button kind={'ghost'} size={'large'} icon={IconAdd} dataId={'btnAdd'} on:click={createTagElementPopup} />
{/if}
</div>
<div class="scroll">
<div class="box">
@@ -120,6 +120,7 @@
resultQuery = mergeQueries(query, e.detail)
}}
/>
<Component
is={viewlet.$lookup.descriptor.component}
props={{
@@ -129,12 +129,14 @@ export const FileExtension = FileNode.extend<FileOptions>({
const fileId = node.attrs['file-id'] ?? ''
if (fileId === '') return
const fileName = node.attrs['data-file-name'] ?? ''
const fileType = node.attrs['data-file-type'] ?? ''
showPopup(
FilePreviewPopup,
{
file: fileId,
name: fileName,
contentType: fileType,
fullSize: false,
showIcon: false
},
@@ -151,12 +151,14 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
const fileId = node.attrs['file-id'] ?? node.attrs.src
const fileName = node.attrs.alt ?? ''
const fileType = node.attrs['data-file-type'] ?? ''
showPopup(
FilePreviewPopup,
{
file: fileId,
name: fileName,
contentType: fileType,
fullSize: true,
showIcon: false
},
@@ -209,10 +211,22 @@ export async function openImage (editor: Editor): Promise<void> {
const attributes = editor.getAttributes('image')
const fileId = attributes['file-id'] ?? attributes.src
const fileName = attributes.alt ?? ''
const fileType = attributes['data-file-type'] ?? ''
await new Promise<void>((resolve) => {
showPopup(FilePreviewPopup, { file: fileId, name: fileName, fullSize: true, showIcon: false }, 'centered', () => {
resolve()
})
showPopup(
FilePreviewPopup,
{
file: fileId,
name: fileName,
contentType: fileType,
fullSize: true,
showIcon: false
},
'centered',
() => {
resolve()
}
)
})
}
@@ -12,26 +12,26 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type Class, type Space, type Doc, type Ref } from '@hcengineering/core'
import { type Class, type Doc, type Ref, type Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getBlobRef, getClient } from '@hcengineering/presentation'
import { CodeBlockExtension, codeBlockOptions, CodeExtension, codeOptions } 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'
import ListKeymap from '@tiptap/extension-list-keymap'
import TableHeader from '@tiptap/extension-table-header'
import 'prosemirror-codemark/dist/codemark.css'
import { getBlobRef, getClient } from '@hcengineering/presentation'
import { CodeBlockExtension, codeBlockOptions, CodeExtension, codeOptions } from '@hcengineering/text'
import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor'
import { DefaultKit, type DefaultKitOptions } from './default-kit'
import { HardBreakExtension } from '../components/extension/hardBreak'
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
import { HardBreakExtension } from '../components/extension/hardBreak'
import { ImageExtension, type ImageOptions } from '../components/extension/imageExt'
import { NodeUuidExtension } from '../components/extension/nodeUuid'
import { Table, TableCell, TableRow } from '../components/extension/table'
import { SubmitExtension, type SubmitOptions } from '../components/extension/submit'
import { ParagraphExtension } from '../components/extension/paragraph'
import { InlineToolbarExtension } from '../components/extension/inlineToolbar'
import { NodeUuidExtension } from '../components/extension/nodeUuid'
import { ParagraphExtension } from '../components/extension/paragraph'
import { SubmitExtension, type SubmitOptions } from '../components/extension/submit'
import { Table, TableCell, TableRow } from '../components/extension/table'
import { DefaultKit, type DefaultKitOptions } from './default-kit'
export interface EditorKitOptions extends DefaultKitOptions {
history?: false
@@ -225,7 +225,7 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
inline: true,
loadingImgSrc:
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgd2lkdGg9IjMycHgiIGhlaWdodD0iMzJweCIgdmlld0JveD0iMCAwIDE2IDE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im0gNCAxIGMgLTEuNjQ0NTMxIDAgLTMgMS4zNTU0NjkgLTMgMyB2IDEgaCAxIHYgLTEgYyAwIC0xLjEwOTM3NSAwLjg5MDYyNSAtMiAyIC0yIGggMSB2IC0xIHogbSAyIDAgdiAxIGggNCB2IC0xIHogbSA1IDAgdiAxIGggMSBjIDEuMTA5Mzc1IDAgMiAwLjg5MDYyNSAyIDIgdiAxIGggMSB2IC0xIGMgMCAtMS42NDQ1MzEgLTEuMzU1NDY5IC0zIC0zIC0zIHogbSAtNSA0IGMgLTAuNTUwNzgxIDAgLTEgMC40NDkyMTkgLTEgMSBzIDAuNDQ5MjE5IDEgMSAxIHMgMSAtMC40NDkyMTkgMSAtMSBzIC0wLjQ0OTIxOSAtMSAtMSAtMSB6IG0gLTUgMSB2IDQgaCAxIHYgLTQgeiBtIDEzIDAgdiA0IGggMSB2IC00IHogbSAtNC41IDIgbCAtMiAyIGwgLTEuNSAtMSBsIC0yIDIgdiAwLjUgYyAwIDAuNSAwLjUgMC41IDAuNSAwLjUgaCA3IHMgMC40NzI2NTYgLTAuMDM1MTU2IDAuNSAtMC41IHYgLTEgeiBtIC04LjUgMyB2IDEgYyAwIDEuNjQ0NTMxIDEuMzU1NDY5IDMgMyAzIGggMSB2IC0xIGggLTEgYyAtMS4xMDkzNzUgMCAtMiAtMC44OTA2MjUgLTIgLTIgdiAtMSB6IG0gMTMgMCB2IDEgYyAwIDEuMTA5Mzc1IC0wLjg5MDYyNSAyIC0yIDIgaCAtMSB2IDEgaCAxIGMgMS42NDQ1MzEgMCAzIC0xLjM1NTQ2OSAzIC0zIHYgLTEgeiBtIC04IDMgdiAxIGggNCB2IC0xIHogbSAwIDAiIGZpbGw9IiMyZTM0MzQiIGZpbGwtb3BhY2l0eT0iMC4zNDkwMiIvPg0KPC9zdmc+DQo=',
getBlobRef: async (file, name, size) => await getBlobRef(undefined, file, name, size),
getBlobRef: async (file, name, size) => await getBlobRef(file, name, size),
HTMLAttributes: this.options.image?.HTMLAttributes ?? {},
...this.options.image
}
@@ -12,14 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import core, {
collaborativeDocParse,
type Blob,
type BlobLookup,
type CollaborativeDoc,
type Ref
} from '@hcengineering/core'
import { getBlobHref, getClient } from '@hcengineering/presentation'
import { collaborativeDocParse, type Blob, type CollaborativeDoc, type Ref } from '@hcengineering/core'
import { getFileUrl } from '@hcengineering/presentation'
import { ObservableV2 as Observable } from 'lib0/observable'
import { applyUpdate, type Doc as YDoc } from 'yjs'
@@ -39,11 +33,7 @@ async function fetchContent (blob: Ref<Blob>, doc: YDoc): Promise<boolean> {
async function fetchBlobContent (_id: Ref<Blob>): Promise<Uint8Array | undefined> {
try {
const blob = (await getClient().findOne(core.class.Blob, { _id })) as BlobLookup
if (blob === undefined || blob.size === 0) {
return undefined
}
const href = await getBlobHref(blob, _id)
const href = getFileUrl(_id)
const res = await fetch(href)
if (res.ok) {
@@ -58,6 +58,7 @@
icon={IconChevronLeft}
kind={'secondary'}
size={'small'}
dataId={'btnPrev'}
on:click={() => {
inc(-1)
}}
@@ -69,6 +70,7 @@
type={!doubleRow ? 'type-button' : 'type-button-icon'}
kind={'secondary'}
size={'small'}
dataId={'btnToday'}
inheritFont
hasMenu
disabled={isToday}
@@ -80,6 +82,7 @@
icon={IconChevronRight}
kind={'secondary'}
size={'small'}
dataId={'btnNext'}
on:click={() => {
inc(1)
}}
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import core, { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { Component } from '@hcengineering/tracker'
import { Loading, Component as ViewComponent } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
@@ -34,6 +34,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: viewlet._id
},
(res) => {
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import core, { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue } from '@hcengineering/tracker'
import { Button, Chevron, ExpandCollapse, IconAdd, closeTooltip, resizeObserver, showPopup } from '@hcengineering/ui'
@@ -77,6 +77,7 @@
preferenceQuery.query(
view.class.ViewletPreference,
{
space: core.space.Workspace,
attachedTo: { $in: configurationRaw.map((it) => it._id) }
},
(res) => {

Some files were not shown because too many files have changed in this diff Show More