Sidebar attachments preview (#6797)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-10-03 19:30:32 +04:00 committed by GitHub
parent e09a24cbc1
commit d3a7759ef7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 376 additions and 83 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Ref, Blob } from '@hcengineering/core'
import { Button } from '@hcengineering/ui'
import { getFileUrl } from '../file'
import Download from './icons/Download.svelte'
import presentation from '../plugin'
export let file: Ref<Blob> | undefined
export let name: string
let download: HTMLAnchorElement
$: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
</script>
{#await srcRef then src}
{#if src !== ''}
<a class="no-line" href={src} download={name} bind:this={download}>
<Button
icon={Download}
kind={'icon'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
{/if}
{/await}

View File

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

View File

@ -0,0 +1,42 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let name: string
function iconLabel (name: string): string {
const parts = `${name}`.split('.')
const ext = parts[parts.length - 1]
return ext.substring(0, 4).toUpperCase()
}
</script>
<div class="flex-center icon">
{iconLabel(name)}
</div>
<style lang="scss">
.icon {
position: relative;
flex-shrink: 0;
width: 2rem;
height: 2rem;
font-weight: 500;
font-size: 0.625rem;
color: var(--primary-button-color);
background-color: var(--primary-button-default);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,41 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Blob, Ref } from '@hcengineering/core'
import { BlobMetadata } from '@hcengineering/presentation'
import { Button, closePopup, closeTooltip, IconDetailsFilled } from '@hcengineering/ui'
import workbench from '@hcengineering/workbench'
import { openFilePreviewInSidebar } from '../utils'
export let file: Ref<Blob> | undefined
export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined
</script>
{#if file}
<Button
icon={IconDetailsFilled}
kind="icon"
on:click={() => {
if (file === undefined) return
closeTooltip()
closePopup()
void openFilePreviewInSidebar(file, name, contentType, metadata)
}}
showTooltip={{ label: workbench.string.OpenInSidebar }}
/>
{/if}

View File

@ -0,0 +1,68 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import workbench, { Widget, WidgetTab } from '@hcengineering/workbench'
import { FilePreview, DownloadFileButton, FilePreviewPopup, FileTypeIcon } from '@hcengineering/presentation'
import { Breadcrumbs, Button, closeTooltip, Header, IconOpen, showPopup } from '@hcengineering/ui'
import { getResource } from '@hcengineering/platform'
import view from '@hcengineering/view'
import attachment from '../plugin'
export let widget: Widget
export let tab: WidgetTab
$: file = tab.data?.file
$: fileName = tab.data?.name ?? ''
$: contentType = tab.data?.contentType
$: metadata = tab.data?.metadata
async function closeTab (): Promise<void> {
const fn = await getResource(workbench.function.CloseWidgetTab)
await fn(widget, tab.id)
}
</script>
<Header
allowFullsize={false}
type="type-aside"
hideBefore={true}
hideActions={false}
hideDescription={true}
adaptive="disabled"
closeOnEscape={false}
on:close={closeTab}
>
<Breadcrumbs
items={[{ title: fileName, icon: FileTypeIcon, iconProps: { name: fileName }, iconMargin: '0 0.5rem 0 0' }]}
currentOnly
/>
<svelte:fragment slot="actions">
<DownloadFileButton name={fileName} {file} />
<Button
icon={view.icon.Open}
kind="icon"
showTooltip={{ label: attachment.string.OpenInWindow }}
on:click={() => {
closeTooltip()
showPopup(FilePreviewPopup, { file, name: fileName, contentType, metadata }, 'centered')
}}
/>
</svelte:fragment>
</Header>
{#if file}
<FilePreview {file} {contentType} name={fileName} {metadata} />
{/if}

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

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

View File

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

View File

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

View File

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