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

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-09-23 18:25:03 +07:00
commit 71ad788949
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
22 changed files with 178 additions and 112 deletions

View File

@ -19,6 +19,7 @@ import notification, { notificationActionTemplates } from '@hcengineering/model-
import activity from '@hcengineering/activity' import activity from '@hcengineering/activity'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import core from '@hcengineering/model-core' import core from '@hcengineering/model-core'
import contact from '@hcengineering/contact'
import chunter from './plugin' import chunter from './plugin'
@ -43,6 +44,18 @@ export function defineActions (builder: Builder): void {
chunter.category.Chunter chunter.category.Chunter
) )
createAction(builder, {
action: chunter.actionImpl.StartConversation,
label: chunter.string.StartConversation,
icon: chunter.icon.Thread,
input: 'focus',
category: chunter.category.Chunter,
target: contact.mixin.Employee,
context: {
mode: 'context',
group: 'associate'
}
})
defineMessageActions(builder) defineMessageActions(builder)
defineChannelActions(builder) defineChannelActions(builder)
} }

View File

@ -53,7 +53,8 @@ export default mergeIds(chunterId, chunter, {
ReplyToThread: '' as ViewAction, ReplyToThread: '' as ViewAction,
OpenInSidebar: '' as ViewAction, OpenInSidebar: '' as ViewAction,
TranslateMessage: '' as ViewAction, TranslateMessage: '' as ViewAction,
ShowOriginalMessage: '' as ViewAction ShowOriginalMessage: '' as ViewAction,
StartConversation: '' as ViewAction
}, },
category: { category: {
Chunter: '' as Ref<ActionCategory> Chunter: '' as Ref<ActionCategory>

View File

@ -27,6 +27,7 @@
import { getFileUrl } from '../file' import { getFileUrl } from '../file'
import { getPreviewType, previewTypes } from '../filetypes' import { getPreviewType, previewTypes } from '../filetypes'
import { imageSizeToRatio } from '../image'
import { BlobMetadata, FilePreviewExtension } from '../types' import { BlobMetadata, FilePreviewExtension } from '../types'
export let file: Ref<Blob> export let file: Ref<Blob>
@ -68,8 +69,8 @@
return return
} }
const pR: number = mD?.pixelRatio ?? 1 const pR: number = mD?.pixelRatio ?? 1
const fWidth: number = mD.originalWidth / pR const fWidth: number = imageSizeToRatio(mD.originalWidth, pR)
const fHeight: number = mD.originalHeight / pR const fHeight: number = imageSizeToRatio(mD.originalHeight, pR)
let mHeight: number = 0 let mHeight: number = 0
let scale: number = 1 let scale: number = 1
if (fWidth > pWidth) { if (fWidth > pWidth) {

View File

@ -15,6 +15,11 @@
import extract from 'png-chunks-extract' import extract from 'png-chunks-extract'
export function imageSizeToRatio (width: number, pixelRatio: number): number {
// consider pixel ratio < 2 as non retina and display them in original size
return pixelRatio < 2 ? width : Math.round(width / pixelRatio)
}
export async function getImageSize (file: Blob): Promise<{ width: number, height: number, pixelRatio: number }> { export async function getImageSize (file: Blob): Promise<{ width: number, height: number, pixelRatio: number }> {
const size = isPng(file) ? await getPngImageSize(file) : undefined const size = isPng(file) ? await getPngImageSize(file) : undefined
@ -65,20 +70,18 @@ async function getPngImageSize (file: Blob): Promise<{ width: number, height: nu
const idhrData = parseIHDR(new DataView(iHDRChunk.data.buffer)) const idhrData = parseIHDR(new DataView(iHDRChunk.data.buffer))
const physData = parsePhys(new DataView(pHYsChunk.data.buffer)) const physData = parsePhys(new DataView(pHYsChunk.data.buffer))
if (physData.unit === 0 && physData.ppux === physData.ppuy) { // Assuming pixels are square
const pixelRatio = Math.round(physData.ppux / 2834.5) // http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Pixel-dimensions
return { const pixelRatio = Math.round(physData.ppux * 0.0254) / 72
width: idhrData.width, return {
height: idhrData.height, width: idhrData.width,
pixelRatio height: idhrData.height,
} pixelRatio
} }
} catch (err) { } catch (err) {
console.error(err) console.error(err)
return undefined return undefined
} }
return undefined
} }
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html // See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html

View File

@ -148,6 +148,8 @@
} }
.icon { .icon {
writing-mode: initial;
text-orientation: initial;
&.vertical { &.vertical {
transform: rotate(90deg); transform: rotate(90deg);
text-orientation: upright; text-orientation: upright;

View File

@ -126,6 +126,7 @@
"TranslateMessage": "Translate message", "TranslateMessage": "Translate message",
"Translate": "Translate", "Translate": "Translate",
"ShowOriginal": "Show original", "ShowOriginal": "Show original",
"Translating": "Translating..." "Translating": "Translating...",
"StartConversation": "Start conversation"
} }
} }

View File

@ -126,6 +126,7 @@
"TranslateMessage": "Traducir mensaje", "TranslateMessage": "Traducir mensaje",
"Translate": "Traducir", "Translate": "Traducir",
"ShowOriginal": "Mostrar original", "ShowOriginal": "Mostrar original",
"Translating": "Traduciendo..." "Translating": "Traduciendo...",
"StartConversation": "Iniciar conversación"
} }
} }

View File

@ -126,6 +126,7 @@
"TranslateMessage": "Traduire le message", "TranslateMessage": "Traduire le message",
"Translate": "Traduire", "Translate": "Traduire",
"ShowOriginal": "Afficher l'original", "ShowOriginal": "Afficher l'original",
"Translating": "Traduction en cours..." "Translating": "Traduction en cours...",
"StartConversation": "Démarrer la conversation"
} }
} }

View File

@ -126,6 +126,7 @@
"TranslateMessage": "Traduzir mensagem", "TranslateMessage": "Traduzir mensagem",
"Translate": "Traduzir", "Translate": "Traduzir",
"ShowOriginal": "Mostrar original", "ShowOriginal": "Mostrar original",
"Translating": "A traduzir..." "Translating": "A traduzir...",
"StartConversation": "Iniciar conversa"
} }
} }

View File

@ -126,6 +126,7 @@
"TranslateMessage": "Перевести сообщение", "TranslateMessage": "Перевести сообщение",
"Translate": "Перевести", "Translate": "Перевести",
"ShowOriginal": "Показать оригинал", "ShowOriginal": "Показать оригинал",
"Translating": "Перевод..." "Translating": "Перевод...",
"StartConversation": "Начать диалог"
} }
} }

View File

@ -126,6 +126,7 @@
"TranslateMessage": "翻译消息", "TranslateMessage": "翻译消息",
"Translate": "翻译", "Translate": "翻译",
"ShowOriginal": "显示原文", "ShowOriginal": "显示原文",
"Translating": "翻译中..." "Translating": "翻译中...",
"StartConversation": "开始对话"
} }
} }

View File

@ -14,24 +14,20 @@
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { deepEqual } from 'fast-equals'
import { DirectMessage } from '@hcengineering/chunter' import contact, { Employee, PersonAccount } from '@hcengineering/contact'
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' import { Ref } from '@hcengineering/core'
import core, { getCurrentAccount, Ref } from '@hcengineering/core' import { SelectUsersPopup } from '@hcengineering/contact-resources'
import { personIdByAccountId, SelectUsersPopup } from '@hcengineering/contact-resources'
import notification from '@hcengineering/notification'
import presentation, { createQuery, getClient } from '@hcengineering/presentation' import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { Modal, showPopup } from '@hcengineering/ui' import { Modal, showPopup } from '@hcengineering/ui'
import chunter from '../../../plugin' import chunter from '../../../plugin'
import { buildDmName } from '../../../utils' import { buildDmName, createDirect } from '../../../utils'
import ChannelMembers from '../../ChannelMembers.svelte' import ChannelMembers from '../../ChannelMembers.svelte'
import { openChannel } from '../../../navigation' import { openChannel } from '../../../navigation'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
const myAcc = getCurrentAccount() as PersonAccount
const query = createQuery() const query = createQuery()
let employeeIds: Ref<Employee>[] = [] let employeeIds: Ref<Employee>[] = []
@ -51,74 +47,12 @@
} }
async function createDirectMessage (): Promise<void> { async function createDirectMessage (): Promise<void> {
const employeeAccounts = await client.findAll(contact.class.PersonAccount, { person: { $in: employeeIds } }) const dmId = await createDirect(employeeIds)
const accIds = [myAcc._id, ...employeeAccounts.filter(({ _id }) => _id !== myAcc._id).map(({ _id }) => _id)].sort()
const existingDms = await client.findAll(chunter.class.DirectMessage, {}) if (dmId !== undefined) {
const newDirectPersons = Array.from(new Set([...employeeIds, myAcc.person])).sort()
let direct: DirectMessage | undefined
for (const dm of existingDms) {
const existDirectPersons = Array.from(
new Set(dm.members.map((id) => $personIdByAccountId.get(id as Ref<PersonAccount>)))
)
.filter((person): person is Ref<Person> => person !== undefined)
.sort()
if (deepEqual(existDirectPersons, newDirectPersons)) {
direct = dm
break
}
}
const existingMembers = direct?.members
const missingAccounts = existingMembers !== undefined ? accIds.filter((id) => !existingMembers.includes(id)) : []
if (direct !== undefined && missingAccounts.length > 0) {
await client.updateDoc(chunter.class.DirectMessage, direct.space, direct._id, {
$push: { members: { $each: missingAccounts, $position: 0 } }
})
}
const dmId =
direct?._id ??
(await client.createDoc(chunter.class.DirectMessage, core.space.Space, {
name: '',
description: '',
private: true,
archived: false,
members: accIds
}))
const context = await client.findOne(notification.class.DocNotifyContext, {
user: myAcc._id,
objectId: dmId,
objectClass: chunter.class.DirectMessage
})
if (context !== undefined) {
if (context.hidden) {
await client.updateDoc(context._class, context.space, context._id, { hidden: false })
}
dispatch('close')
openChannel(dmId, chunter.class.DirectMessage) openChannel(dmId, chunter.class.DirectMessage)
return dispatch('close')
} }
const space = await client.findOne(contact.class.PersonSpace, { person: myAcc.person }, { projection: { _id: 1 } })
if (!space) return
await client.createDoc(notification.class.DocNotifyContext, space._id, {
user: myAcc._id,
objectId: dmId,
objectClass: chunter.class.DirectMessage,
objectSpace: core.space.Space,
hidden: false,
isPinned: false
})
openChannel(dmId, chunter.class.DirectMessage)
dispatch('close')
} }
function handleCancel (): void { function handleCancel (): void {

View File

@ -78,7 +78,8 @@ import {
removeChannelAction, removeChannelAction,
translateMessage, translateMessage,
showOriginalMessage, showOriginalMessage,
canTranslateMessage canTranslateMessage,
startConversationAction
} from './utils' } from './utils'
export { default as ChatMessageInput } from './components/chat-message/ChatMessageInput.svelte' export { default as ChatMessageInput } from './components/chat-message/ChatMessageInput.svelte'
@ -213,6 +214,7 @@ export default async (): Promise<Resources> => ({
ReplyToThread: replyToThread, ReplyToThread: replyToThread,
OpenInSidebar: openChannelInSidebarAction, OpenInSidebar: openChannelInSidebarAction,
TranslateMessage: translateMessage, TranslateMessage: translateMessage,
ShowOriginalMessage: showOriginalMessage ShowOriginalMessage: showOriginalMessage,
StartConversation: startConversationAction
} }
}) })

View File

@ -22,7 +22,7 @@ import activity, {
import { isReactionMessage } from '@hcengineering/activity-resources' import { isReactionMessage } from '@hcengineering/activity-resources'
import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter' import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
import contact, { getName, type Employee, type Person, type PersonAccount } from '@hcengineering/contact' import contact, { getName, type Employee, type Person, type PersonAccount } from '@hcengineering/contact'
import { PersonIcon, employeeByIdStore } from '@hcengineering/contact-resources' import { PersonIcon, employeeByIdStore, personIdByAccountId } from '@hcengineering/contact-resources'
import core, { import core, {
getCurrentAccount, getCurrentAccount,
type Account, type Account,
@ -35,7 +35,7 @@ import core, {
type Timestamp, type Timestamp,
type WithLookup type WithLookup
} from '@hcengineering/core' } from '@hcengineering/core'
import { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { import {
InboxNotificationsClientImpl, InboxNotificationsClientImpl,
isActivityNotification, isActivityNotification,
@ -48,10 +48,11 @@ import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-res
import { get, writable, type Unsubscriber } from 'svelte/store' import { get, writable, type Unsubscriber } from 'svelte/store'
import aiBot from '@hcengineering/ai-bot' import aiBot from '@hcengineering/ai-bot'
import { translate as aiTranslate } from '@hcengineering/ai-bot-resources' import { translate as aiTranslate } from '@hcengineering/ai-bot-resources'
import { deepEqual } from 'fast-equals'
import ChannelIcon from './components/ChannelIcon.svelte' import ChannelIcon from './components/ChannelIcon.svelte'
import DirectIcon from './components/DirectIcon.svelte' import DirectIcon from './components/DirectIcon.svelte'
import { resetChunterLocIfEqual } from './navigation' import { openChannelInSidebar, resetChunterLocIfEqual } from './navigation'
import chunter from './plugin' import chunter from './plugin'
import { shownTranslatedMessagesStore, translatedMessagesStore, translatingMessagesStore } from './stores' import { shownTranslatedMessagesStore, translatedMessagesStore, translatingMessagesStore } from './stores'
@ -560,3 +561,90 @@ export async function canTranslateMessage (): Promise<boolean> {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? '' const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
return url !== '' return url !== ''
} }
export async function startConversationAction (docs?: Employee | Employee[]): Promise<void> {
if (docs === undefined) return
const employees = Array.isArray(docs) ? docs : [docs]
const employeeIds = employees.map(({ _id }) => _id)
const dm = await createDirect(employeeIds)
if (dm !== undefined) {
await openChannelInSidebar(dm, chunter.class.DirectMessage, undefined, undefined, true)
}
}
export async function createDirect (employeeIds: Array<Ref<Employee>>): Promise<Ref<DirectMessage>> {
const client = getClient()
const me = getCurrentAccount()
const employeeAccounts = await client.findAll(contact.class.PersonAccount, { person: { $in: employeeIds } })
const accIds = [me._id, ...employeeAccounts.filter(({ _id }) => _id !== me._id).map(({ _id }) => _id)].sort()
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
const newDirectPersons = Array.from(new Set([...employeeIds, me.person] as Array<Ref<Person>>)).sort()
let direct: DirectMessage | undefined
for (const dm of existingDms) {
const existDirectPersons = Array.from(
new Set(dm.members.map((id) => get(personIdByAccountId).get(id as Ref<PersonAccount>)))
)
.filter((person): person is Ref<Person> => person !== undefined)
.sort()
if (deepEqual(existDirectPersons, newDirectPersons)) {
direct = dm
break
}
}
const existingMembers = direct?.members
const missingAccounts = existingMembers !== undefined ? accIds.filter((id) => !existingMembers.includes(id)) : []
if (direct !== undefined && missingAccounts.length > 0) {
await client.updateDoc(chunter.class.DirectMessage, direct.space, direct._id, {
$push: { members: { $each: missingAccounts, $position: 0 } }
})
}
const dmId =
direct?._id ??
(await client.createDoc(chunter.class.DirectMessage, core.space.Space, {
name: '',
description: '',
private: true,
archived: false,
members: accIds
}))
const context = await client.findOne(notification.class.DocNotifyContext, {
user: me._id,
objectId: dmId,
objectClass: chunter.class.DirectMessage
})
if (context !== undefined) {
if (context.hidden) {
await client.updateDoc(context._class, context.space, context._id, { hidden: false })
}
return dmId
}
const space = await client.findOne(
contact.class.PersonSpace,
{ person: me.person as Ref<Person> },
{ projection: { _id: 1 } }
)
if (space == null) return dmId
await client.createDoc(notification.class.DocNotifyContext, space._id, {
user: me._id,
objectId: dmId,
objectClass: chunter.class.DirectMessage,
objectSpace: core.space.Space,
hidden: false,
isPinned: false
})
return dmId
}

View File

@ -208,7 +208,8 @@ export default plugin(chunterId, {
TranslateMessage: '' as IntlString, TranslateMessage: '' as IntlString,
Translate: '' as IntlString, Translate: '' as IntlString,
ShowOriginal: '' as IntlString, ShowOriginal: '' as IntlString,
Translating: '' as IntlString Translating: '' as IntlString,
StartConversation: '' as IntlString
}, },
ids: { ids: {
DMNotification: '' as Ref<NotificationType>, DMNotification: '' as Ref<NotificationType>,

View File

@ -17,7 +17,7 @@
<script lang="ts"> <script lang="ts">
import { type Space, type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core' import { type Space, type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform' import { IntlString, translate } from '@hcengineering/platform'
import { getFileUrl, getImageSize } from '@hcengineering/presentation' import { getFileUrl, getImageSize, imageSizeToRatio } from '@hcengineering/presentation'
import { markupToJSON } from '@hcengineering/text' import { markupToJSON } from '@hcengineering/text'
import { import {
AnySvelteComponent, AnySvelteComponent,
@ -297,7 +297,7 @@
type: 'image', type: 'image',
attrs: { attrs: {
'file-id': attached.file, 'file-id': attached.file,
width: Math.round(size.width / size.pixelRatio) width: imageSizeToRatio(size.width, size.pixelRatio)
} }
}, },
{ {

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Markup } from '@hcengineering/core' import { Markup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import presentation, { MessageViewer, getFileUrl, getImageSize } from '@hcengineering/presentation' import presentation, { MessageViewer, getFileUrl, getImageSize, imageSizeToRatio } from '@hcengineering/presentation'
import { EmptyMarkup } from '@hcengineering/text' import { EmptyMarkup } from '@hcengineering/text'
import textEditor, { RefAction } from '@hcengineering/text-editor' import textEditor, { RefAction } from '@hcengineering/text-editor'
import { import {
@ -257,7 +257,7 @@
type: 'image', type: 'image',
attrs: { attrs: {
'file-id': attached.file, 'file-id': attached.file,
width: Math.round(size.width / size.pixelRatio) width: imageSizeToRatio(size.width, size.pixelRatio)
} }
}, },
{ {

View File

@ -153,12 +153,15 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
const fileName = node.attrs.alt ?? '' const fileName = node.attrs.alt ?? ''
const fileType = node.attrs['data-file-type'] ?? 'image/*' const fileType = node.attrs['data-file-type'] ?? 'image/*'
const metadata = node.attrs.width !== undefined ? { originalWidth: node.attrs.width } : {}
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: fileId, file: fileId,
name: fileName, name: fileName,
contentType: fileType, contentType: fileType,
metadata,
fullSize: true, fullSize: true,
showIcon: false showIcon: false
}, },

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { setPlatformStatus, unknownError } from '@hcengineering/platform' import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getImageSize } from '@hcengineering/presentation' import { imageSizeToRatio, getImageSize } from '@hcengineering/presentation'
import { Extension } from '@tiptap/core' import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state' import { Plugin, PluginKey } from '@tiptap/pm/state'
import { type EditorView } from '@tiptap/pm/view' import { type EditorView } from '@tiptap/pm/view'
@ -41,6 +41,8 @@ function getType (type: string): 'image' | 'other' {
* @public * @public
*/ */
export const ImageUploadExtension = Extension.create<ImageUploadOptions>({ export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
name: 'image-upload',
addOptions () { addOptions () {
return { return {
getFileUrl: () => '' getFileUrl: () => ''
@ -157,7 +159,7 @@ async function handleImageUpload (
src: url, src: url,
alt: file.name, alt: file.name,
title: file.name, title: file.name,
width: Math.round(size.width / size.pixelRatio) width: imageSizeToRatio(size.width, size.pixelRatio)
}) })
const transaction = view.state.tr.insert(pos?.pos ?? 0, node) const transaction = view.state.tr.insert(pos?.pos ?? 0, node)

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { getBlobRef, type BlobMetadata } from '@hcengineering/presentation' import { getBlobRef, imageSizeToRatio, type BlobMetadata } from '@hcengineering/presentation'
import { Loading } from '@hcengineering/ui' import { Loading } from '@hcengineering/ui'
export let value: Ref<Blob> export let value: Ref<Blob>
@ -22,15 +22,20 @@
export let metadata: BlobMetadata | undefined export let metadata: BlobMetadata | undefined
export let fit: boolean = false export let fit: boolean = false
$: p = getBlobRef(value, name) $: originalWidth = metadata?.originalWidth
$: width = metadata?.originalWidth ? `min(${metadata.originalWidth / metadata?.pixelRatio ?? 1}px, 100%)` : '100%' $: originalHeight = metadata?.originalHeight
$: height = metadata?.originalHeight $: pixelRatio = metadata?.pixelRatio ?? 1
? `min(${metadata.originalHeight / metadata?.pixelRatio ?? 1}px, ${fit ? '100%' : '80vh'})`
: '100%' $: imageWidth = originalWidth != null ? imageSizeToRatio(originalWidth, pixelRatio) : undefined
$: imageHeight = originalHeight != null ? imageSizeToRatio(originalHeight, pixelRatio) : undefined
$: width = imageWidth != null ? `min(${imageWidth}px, 100%)` : '100%'
$: height = imageHeight != null ? `min(${imageHeight}px, ${fit ? '100%' : '80vh'})` : '100%'
let loading = true let loading = true
</script> </script>
{#await p then blobRef} {#await getBlobRef(value, name) then blobRef}
{#if loading} {#if loading}
<div class="flex-center w-full h-full clear-mins"> <div class="flex-center w-full h-full clear-mins">
<Loading /> <Loading />

View File

@ -23,4 +23,4 @@
export let selected: Ref<Widget> | undefined = undefined export let selected: Ref<Widget> | undefined = undefined
</script> </script>
<WidgetsBar {widgets} {preferences} {selected} /> <WidgetsBar {widgets} {preferences} {selected} roundBorder />

View File

@ -24,6 +24,7 @@
export let widgets: Widget[] = [] export let widgets: Widget[] = []
export let preferences: WidgetPreference[] = [] export let preferences: WidgetPreference[] = []
export let selected: Ref<Widget> | undefined = undefined export let selected: Ref<Widget> | undefined = undefined
export let roundBorder = false
function handleAddWidget (): void { function handleAddWidget (): void {
showPopup(AddWidgetsPopup, { widgets }) showPopup(AddWidgetsPopup, { widgets })
@ -48,7 +49,7 @@
.filter((widget): widget is Widget => widget !== undefined && widget.type === WidgetType.Configurable) .filter((widget): widget is Widget => widget !== undefined && widget.type === WidgetType.Configurable)
</script> </script>
<div class="root"> <div class="root" class:roundBorder>
<div class="block"> <div class="block">
{#each fixedWidgets as widget} {#each fixedWidgets as widget}
<WidgetPresenter <WidgetPresenter
@ -103,6 +104,10 @@
max-width: 3.5rem; max-width: 3.5rem;
border-top: 1px solid var(--theme-divider-color); border-top: 1px solid var(--theme-divider-color);
overflow-y: auto; overflow-y: auto;
&.roundBorder {
border-top-left-radius: var(--small-focus-BorderRadius);
}
} }
.block { .block {