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-24 20:02:08 +07:00
commit f72dd3b513
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
28 changed files with 610 additions and 507 deletions

View File

@ -1140,7 +1140,8 @@ export function devTool (
.option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 50mb)', '50') .option('-bl, --blobLimit <blobLimit>', 'A blob size limit in megabytes (default 50mb)', '50')
.option('-c, --concurrency <concurrency>', 'Number of files being processed concurrently', '10') .option('-c, --concurrency <concurrency>', 'Number of files being processed concurrently', '10')
.option('--disabled', 'Include disabled workspaces', false) .option('--disabled', 'Include disabled workspaces', false)
.action(async (cmd: { workspace: string, move: string, blobLimit: string, concurrency: string, disabled: boolean }) => { .action(
async (cmd: { workspace: string, move: string, blobLimit: string, concurrency: string, disabled: boolean }) => {
const params = { const params = {
blobSizeLimitMb: parseInt(cmd.blobLimit), blobSizeLimitMb: parseInt(cmd.blobLimit),
concurrency: parseInt(cmd.concurrency), concurrency: parseInt(cmd.concurrency),
@ -1182,7 +1183,8 @@ export function devTool (
} }
}) })
}) })
}) }
)
program program
.command('sync-files') .command('sync-files')

View File

@ -105,6 +105,10 @@ export class TBrowserNotification extends TDoc implements BrowserNotification {
onClickLocation?: Location | undefined onClickLocation?: Location | undefined
user!: Ref<Account> user!: Ref<Account>
status!: NotificationStatus status!: NotificationStatus
messageId?: Ref<ActivityMessage>
messageClass?: Ref<Class<ActivityMessage>>
objectId!: Ref<Doc>
objectClass!: Ref<Class<Doc>>
} }
@Model(notification.class.PushSubscription, core.class.Doc, DOMAIN_USER_NOTIFY) @Model(notification.class.PushSubscription, core.class.Doc, DOMAIN_USER_NOTIFY)

View File

@ -34,7 +34,6 @@ export default mergeIds(timeId, time, {
EditToDo: '' as IntlString, EditToDo: '' as IntlString,
GotoTimePlaning: '' as IntlString, GotoTimePlaning: '' as IntlString,
GotoTimeTeamPlaning: '' as IntlString, GotoTimeTeamPlaning: '' as IntlString,
NewToDo: '' as IntlString,
Priority: '' as IntlString, Priority: '' as IntlString,
MarkedAsDone: '' as IntlString MarkedAsDone: '' as IntlString
}, },

View File

@ -1,3 +1,177 @@
.hulyButton {
display: inline-flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
gap: var(--spacing-1);
border: 1px solid transparent;
&:not(:disabled, .disabled, .loading) { cursor: pointer; }
&.inheritFont { font: inherit; }
.icon {
display: flex;
align-items: center;
justify-content: center;
width: var(--spacing-2_5);
height: var(--spacing-2_5);
}
span { white-space: nowrap; }
&:focus {
outline: 2px solid var(--global-focus-BorderColor);
outline-offset: 2px;
}
&.type-button-icon { padding: 0; }
&.large {
height: var(--global-large-Size);
border-radius: var(--medium-BorderRadius);
&.round { border-radius: var(--large-BorderRadius); }
&.type-button:not(.iconOnly) { padding: 0 var(--spacing-2); }
&.iconOnly,
&.type-button-icon { width: var(--global-large-Size); }
}
&.medium {
height: var(--global-medium-Size);
border-radius: var(--medium-BorderRadius);
&.round { border-radius: var(--large-BorderRadius); }
&.type-button:not(.iconOnly) { padding: 0 var(--spacing-2); }
&.iconOnly,
&.type-button-icon { width: var(--global-medium-Size); }
}
&.small {
height: var(--global-small-Size);
gap: var(--spacing-0_5);
border-radius: var(--small-BorderRadius);
&.round { border-radius: var(--large-BorderRadius); }
&.type-button:not(.iconOnly) { padding: 0 var(--spacing-1); }
&.iconOnly,
&.type-button-icon { width: var(--global-small-Size); }
}
&.extra-small {
height: var(--global-extra-small-Size);
border-radius: var(--extra-small-BorderRadius);
&.round { border-radius: var(--large-BorderRadius); }
&.type-button:not(.iconOnly) { padding: 0 var(--spacing-1); }
&.iconOnly,
&.type-button-icon { width: var(--global-extra-small-Size); }
}
&.min {
height: var(--global-min-Size);
border: 0;
border-radius: var(--min-BorderRadius);
}
&.type-button-icon .icon,
&.menu .icon {
width: var(--spacing-2);
height: var(--spacing-2);
}
&:disabled:not(.loading),
&.disabled:not(.loading) {
border-color: transparent;
cursor: not-allowed;
.icon { color: var(--button-disabled-IconColor); }
span { color: var(--button-disabled-LabelColor); }
}
&.primary {
border-color: var(--button-primary-BorderColor);
background-color: var(--button-primary-BackgroundColor);
.icon { color: var(--button-accent-IconColor); }
span { color: var(--button-accent-LabelColor); }
&:not(.disabled, :disabled):hover { background-color: var(--button-primary-hover-BackgroundColor); }
&:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) { background-color: var(--button-primary-active-BackgroundColor); }
&.menu:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) { border-color: var(--button-menu-active-BorderColor); }
&:disabled:not(.loading),
&.disabled:not(.loading) { background-color: var(--button-disabled-BackgroundColor); }
&.loading {
background-color: var(--button-primary-active-BackgroundColor);
span { color: var(--button-primary-loading-LabelColor); }
}
}
&.secondary {
border-color: var(--button-secondary-BorderColor);
background-color: var(--button-secondary-BackgroundColor);
.icon { color: var(--button-subtle-IconColor); }
span { color: var(--button-subtle-LabelColor); }
&:not(.disabled, :disabled):hover { background-color: var(--button-secondary-hover-BackgroundColor); }
&:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) { background-color: var(--button-secondary-active-BackgroundColor); }
&.menu:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) { border-color: var(--button-menu-active-BorderColor); }
&:disabled:not(.loading),
&.disabled:not(.loading) { background-color: var(--button-disabled-BackgroundColor); }
&.loading {
background-color: var(--button-secondary-active-BackgroundColor);
span { color: var(--button-disabled-LabelColor); }
}
}
&.tertiary {
border-color: transparent;
background-color: transparent;
&:not(.inheritColor) .icon { color: var(--button-subtle-IconColor); }
&.inheritColor {
color: inherit;
.icon { color: currentColor; }
}
span { color: var(--button-subtle-LabelColor); }
&:not(.disabled, :disabled):hover { background-color: var(--button-tertiary-hover-BackgroundColor); }
&:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) { background-color: var(--button-tertiary-active-BackgroundColor); }
&.menu:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) { border-color: var(--button-menu-active-BorderColor); }
&.loading {
background-color: var(--button-tertiary-active-BackgroundColor);
span { color: var(--button-disabled-LabelColor); }
}
}
&.negative {
border-color: var(--button-negative-BorderColor);
background-color: var(--button-negative-BackgroundColor);
.icon { color: var(--button-accent-IconColor); }
span { color: var(--button-accent-LabelColor); }
&:not(.disabled, :disabled):hover { background-color: var(--button-negative-hover-BackgroundColor); }
&:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) { background-color: var(--button-negative-active-BackgroundColor); }
&.menu:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) { border-color: var(--button-menu-active-BorderColor); }
&:disabled:not(.loading),
&.disabled:not(.loading) { background-color: var(--button-disabled-BackgroundColor); }
&.loading {
background-color: var(--button-negative-active-BackgroundColor);
span { color: var(--button-negative-loading-LabelColor); }
}
}
& > * { pointer-events: none; }
}
// Old style Button
.antiButton { .antiButton {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -92,7 +92,7 @@
<button <button
{id} {id}
bind:this={element} bind:this={element}
class="font-medium-14 {kind} {size} {type} {shape}" class="hulyButton font-medium-14 {kind} {size} {type} {shape}"
class:loading class:loading
class:pressed class:pressed
class:inheritColor class:inheritColor
@ -115,300 +115,3 @@
{#if title}<span>{title}</span>{/if} {#if title}<span>{title}</span>{/if}
<slot /> <slot />
</button> </button>
<style lang="scss">
button {
display: inline-flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
gap: var(--spacing-1);
border: 1px solid transparent;
&:not(:disabled, .loading) {
cursor: pointer;
}
&.inheritFont {
font: inherit;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: var(--spacing-2_5);
height: var(--spacing-2_5);
}
span {
white-space: nowrap;
}
&:focus {
outline: 2px solid var(--global-focus-BorderColor);
outline-offset: 2px;
}
&.type-button-icon {
padding: 0;
}
&.large {
height: var(--global-large-Size);
border-radius: var(--medium-BorderRadius);
&.round {
border-radius: var(--large-BorderRadius);
}
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-2);
}
&.iconOnly,
&.type-button-icon {
width: var(--global-large-Size);
}
}
&.medium {
height: var(--global-medium-Size);
border-radius: var(--medium-BorderRadius);
&.round {
border-radius: var(--large-BorderRadius);
}
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-2);
}
&.iconOnly,
&.type-button-icon {
width: var(--global-medium-Size);
}
}
&.small {
height: var(--global-small-Size);
gap: var(--spacing-0_5);
border-radius: var(--small-BorderRadius);
&.round {
border-radius: var(--large-BorderRadius);
}
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-1);
}
&.iconOnly,
&.type-button-icon {
width: var(--global-small-Size);
}
}
&.extra-small {
height: var(--global-extra-small-Size);
border-radius: var(--extra-small-BorderRadius);
&.round {
border-radius: var(--large-BorderRadius);
}
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-1);
}
&.iconOnly,
&.type-button-icon {
width: var(--global-extra-small-Size);
}
}
&.min {
height: var(--global-min-Size);
border: 0;
border-radius: var(--min-BorderRadius);
}
&.type-button-icon .icon,
&.menu .icon {
width: var(--spacing-2);
height: var(--spacing-2);
}
&.primary {
border-color: var(--button-primary-BorderColor);
background-color: var(--button-primary-BackgroundColor);
.icon {
color: var(--button-accent-IconColor);
}
span {
color: var(--button-accent-LabelColor);
}
&:hover {
background-color: var(--button-primary-hover-BackgroundColor);
}
&:active,
&.pressed {
background-color: var(--button-primary-active-BackgroundColor);
}
&.menu:enabled:active,
&.pressed {
border-color: var(--button-menu-active-BorderColor);
}
&:disabled:not(.loading) {
background-color: var(--button-disabled-BackgroundColor);
border-color: transparent;
cursor: not-allowed;
.icon {
color: var(--button-disabled-IconColor);
}
span {
color: var(--button-disabled-LabelColor);
}
}
&.loading {
background-color: var(--button-primary-active-BackgroundColor);
span {
color: var(--button-primary-loading-LabelColor);
}
}
}
&.secondary {
border-color: var(--button-secondary-BorderColor);
background-color: var(--button-secondary-BackgroundColor);
.icon {
color: var(--button-subtle-IconColor);
}
span {
color: var(--button-subtle-LabelColor);
}
&:hover {
background-color: var(--button-secondary-hover-BackgroundColor);
}
&:active,
&.pressed {
background-color: var(--button-secondary-active-BackgroundColor);
}
&.menu:enabled:active,
&.pressed {
border-color: var(--button-menu-active-BorderColor);
}
&:disabled:not(.loading) {
background-color: var(--button-disabled-BackgroundColor);
border-color: transparent;
cursor: not-allowed;
.icon {
color: var(--button-disabled-IconColor);
}
span {
color: var(--button-disabled-LabelColor);
}
}
&.loading {
background-color: var(--button-secondary-active-BackgroundColor);
span {
color: var(--button-disabled-LabelColor);
}
}
}
&.tertiary {
border-color: transparent;
background-color: transparent;
&:not(.inheritColor) .icon {
color: var(--button-subtle-IconColor);
}
&.inheritColor {
color: inherit;
.icon {
color: currentColor;
}
}
span {
color: var(--button-subtle-LabelColor);
}
&:hover:enabled {
background-color: var(--button-tertiary-hover-BackgroundColor);
}
&:active:enabled,
&.pressed:enabled {
background-color: var(--button-tertiary-active-BackgroundColor);
}
&.menu:active:enabled,
&.pressed:enabled {
border-color: var(--button-menu-active-BorderColor);
}
&:disabled:not(.loading) {
border-color: transparent;
cursor: not-allowed;
.icon {
color: var(--button-disabled-IconColor);
}
span {
color: var(--button-disabled-LabelColor);
}
}
&.loading {
background-color: var(--button-tertiary-active-BackgroundColor);
span {
color: var(--button-disabled-LabelColor);
}
}
}
&.negative {
border-color: var(--button-negative-BorderColor);
background-color: var(--button-negative-BackgroundColor);
.icon {
color: var(--button-accent-IconColor);
}
span {
color: var(--button-accent-LabelColor);
}
&:hover {
background-color: var(--button-negative-hover-BackgroundColor);
}
&:active,
&.pressed {
background-color: var(--button-negative-active-BackgroundColor);
}
&.menu:enabled:active,
&.pressed {
border-color: var(--button-menu-active-BorderColor);
}
&:disabled:not(.loading) {
background-color: var(--button-disabled-BackgroundColor);
border-color: transparent;
cursor: not-allowed;
.icon {
color: var(--button-disabled-IconColor);
}
span {
color: var(--button-disabled-LabelColor);
}
}
&.loading {
background-color: var(--button-negative-active-BackgroundColor);
span {
color: var(--button-negative-loading-LabelColor);
}
}
}
& > * {
pointer-events: none;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
</style>

View File

@ -153,7 +153,7 @@
} }
} }
} }
const focused = (ed: TEdits): void => { export const focused = (ed: TEdits): void => {
selected = ed selected = ed
startTyping = true startTyping = true
} }

View File

@ -31,7 +31,7 @@
export let date: number export let date: number
export let difference: number = 0 export let difference: number = 0
export let direction: 'vertical' | 'horizontal' = 'vertical' export let direction: 'vertical' | 'horizontal' | 'auto' = 'vertical'
export let showDate: boolean = true export let showDate: boolean = true
export let withoutTime: boolean export let withoutTime: boolean
export let kind: ButtonBaseKind = 'tertiary' export let kind: ButtonBaseKind = 'tertiary'
@ -44,8 +44,9 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: currentDate = new Date(date) $: currentDate = new Date(date)
let tib: TimeInputBox
function timeClick (e: MouseEvent) { function timeClick (e: MouseEvent & { currentTarget: EventTarget & HTMLElement }) {
if (!showDate) { if (!showDate) {
showPopup( showPopup(
DatePopup, DatePopup,
@ -58,7 +59,7 @@
} }
} }
) )
} } else if (!disabled) tib.focused(e.offsetX <= e.currentTarget.clientWidth / 2 ? 'hour' : 'min')
} }
function dateClick (e: MouseEvent) { function dateClick (e: MouseEvent) {
@ -85,13 +86,17 @@
{#if fixed === undefined} {#if fixed === undefined}
<div class="min-w-28"> <div class="min-w-28">
<ButtonBase type="type-button" {kind} {size} {disabled} {focusIndex} on:click={dateClick}> <ButtonBase type="type-button" {kind} {size} {disabled} {focusIndex} on:click={dateClick}>
<span class="overflow-label"><DateLocalePresenter date={currentDate.getTime()} {timeZone} /></span> <span class="overflow-label tertiary-textColor">
<DateLocalePresenter date={currentDate.getTime()} {timeZone} />
</span>
</ButtonBase> </ButtonBase>
</div> </div>
{:else} {:else}
<FixedColumn key={fixed + '-date'} addClass={'min-w-28'}> <FixedColumn key={fixed + '-date'} addClass={'min-w-28'}>
<ButtonBase type="type-button" {kind} {size} {disabled} {focusIndex} on:click={dateClick}> <ButtonBase type="type-button" {kind} {size} {disabled} {focusIndex} on:click={dateClick}>
<span class="overflow-label"><DateLocalePresenter date={currentDate.getTime()} {timeZone} /></span> <span class="overflow-label tertiary-textColor">
<DateLocalePresenter date={currentDate.getTime()} {timeZone} />
</span>
</ButtonBase> </ButtonBase>
</FixedColumn> </FixedColumn>
{/if} {/if}
@ -103,15 +108,17 @@
{#if !withoutTime} {#if !withoutTime}
{#if fixed === undefined} {#if fixed === undefined}
<ButtonBase <!-- svelte-ignore a11y-no-static-element-interactions -->
type="type-button" <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
{kind} <!-- svelte-ignore a11y-click-events-have-key-events -->
{size} <div
{disabled} class="hulyButton type-button tertiary small rectangle font-medium-14"
focusIndex={focusIndex !== -1 ? focusIndex + 1 : focusIndex} class:disabled
tabindex={focusIndex !== -1 ? focusIndex + 1 : focusIndex}
on:click={timeClick} on:click={timeClick}
> >
<TimeInputBox <TimeInputBox
bind:this={tib}
bind:currentDate bind:currentDate
{timeZone} {timeZone}
noBorder noBorder
@ -120,18 +127,20 @@
updateTime(date.detail) updateTime(date.detail)
}} }}
/> />
</ButtonBase> </div>
{:else} {:else}
<FixedColumn key={fixed + '-time'} addClass={'min-w-28'}> <FixedColumn key={fixed + '-time'} addClass={'min-w-28'}>
<ButtonBase <!-- svelte-ignore a11y-no-static-element-interactions -->
type="type-button" <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
{kind} <!-- svelte-ignore a11y-click-events-have-key-events -->
{size} <div
{disabled} class="hulyButton type-button tertiary small rectangle font-medium-14"
focusIndex={focusIndex !== -1 ? focusIndex + 1 : focusIndex} class:disabled
tabindex={focusIndex !== -1 ? focusIndex + 1 : focusIndex}
on:click={timeClick} on:click={timeClick}
> >
<TimeInputBox <TimeInputBox
bind:this={tib}
bind:currentDate bind:currentDate
{timeZone} {timeZone}
noBorder noBorder
@ -140,7 +149,7 @@
updateTime(date.detail) updateTime(date.detail)
}} }}
/> />
</ButtonBase> </div>
</FixedColumn> </FixedColumn>
{/if} {/if}
{/if} {/if}
@ -162,9 +171,15 @@
<style lang="scss"> <style lang="scss">
.dateEditor-container { .dateEditor-container {
display: flex; display: flex;
flex-wrap: nowrap;
&.horizontal { &:not(.auto) {
flex-wrap: nowrap;
}
&.auto {
flex-wrap: wrap;
}
&.horizontal,
&.auto {
align-items: center; align-items: center;
} }
&.vertical { &.vertical {

View File

@ -31,14 +31,15 @@
export let isAsideOpened = false export let isAsideOpened = false
export let syncLocation = true export let syncLocation = true
export let freeze = false export let freeze = false
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
let dataProvider: ChannelDataProvider | undefined let dataProvider: ChannelDataProvider | undefined
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
const unsubscribe = messageInFocus.subscribe((id) => { const unsubscribe = messageInFocus.subscribe((id) => {
if (!syncLocation) return
if (id !== undefined && id !== selectedMessageId) { if (id !== undefined && id !== selectedMessageId) {
selectedMessageId = id selectedMessageId = id
} }
@ -47,9 +48,7 @@
}) })
const unsubscribeLocation = locationStore.subscribe((newLocation) => { const unsubscribeLocation = locationStore.subscribe((newLocation) => {
if (!syncLocation) { if (!syncLocation) return
return
}
const id = getMessageFromLoc(newLocation) const id = getMessageFromLoc(newLocation)
selectedMessageId = id selectedMessageId = id
messageInFocus.set(id) messageInFocus.set(id)

View File

@ -582,13 +582,13 @@
} }
async function restoreScroll () { async function restoreScroll () {
if (!scrollElement || !scroller) { await wait()
if (!scrollElement || !scroller || scrollToRestore === 0) {
scrollToRestore = 0 scrollToRestore = 0
return return
} }
await wait()
const delta = scrollElement.scrollHeight - scrollToRestore const delta = scrollElement.scrollHeight - scrollToRestore
scroller.scrollBy(delta) scroller.scrollBy(delta)

View File

@ -18,7 +18,9 @@
import { DocNotifyContext } from '@hcengineering/notification' import { DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources' import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { Widget } from '@hcengineering/workbench' import { Widget } from '@hcengineering/workbench'
import { ActivityMessage } from '@hcengineering/activity'
import { ChatWidgetTab } from '@hcengineering/chunter' import { ChatWidgetTab } from '@hcengineering/chunter'
import { updateTabData } from '@hcengineering/workbench-resources'
import Channel from './Channel.svelte' import Channel from './Channel.svelte'
import { closeThreadInSidebarChannel } from '../navigation' import { closeThreadInSidebarChannel } from '../navigation'
@ -36,12 +38,18 @@
let object: Doc | undefined = undefined let object: Doc | undefined = undefined
let context: DocNotifyContext | undefined = undefined let context: DocNotifyContext | undefined = undefined
let selectedMessageId: Ref<ActivityMessage> | undefined = tab.data.selectedMessageId
$: context = object ? $contextByDocStore.get(object._id) : undefined $: context = object ? $contextByDocStore.get(object._id) : undefined
$: void loadObject(tab.data._id, tab.data._class) $: void loadObject(tab.data._id, tab.data._class)
$: threadId = tab.data.thread $: threadId = tab.data.thread
$: if (tab.data.selectedMessageId !== undefined && tab.data.selectedMessageId !== '') {
selectedMessageId = tab.data.selectedMessageId
updateTabData(widget._id, tab.id, { selectedMessageId: '' })
}
async function loadObject (_id?: Ref<Doc>, _class?: Ref<Class<Doc>>): Promise<void> { async function loadObject (_id?: Ref<Doc>, _class?: Ref<Class<Doc>>): Promise<void> {
if (_id === undefined || _class === undefined) { if (_id === undefined || _class === undefined) {
object = undefined object = undefined
@ -80,13 +88,19 @@
on:close on:close
/> />
{#key object._id} {#key object._id}
<Channel {object} {context} syncLocation={false} freeze={threadId !== undefined} /> <Channel {object} {context} syncLocation={false} freeze={threadId !== undefined} {selectedMessageId} />
{/key} {/key}
</div> </div>
{/if} {/if}
{#if threadId} {#if threadId}
<div class="thread" style:height style:width> <div class="thread" style:height style:width>
<ThreadView _id={threadId} on:channel={() => closeThreadInSidebarChannel(widget, tab)} on:close /> <ThreadView
_id={threadId}
{selectedMessageId}
syncLocation={false}
on:channel={() => closeThreadInSidebarChannel(widget, tab)}
on:close
/>
</div> </div>
{/if} {/if}

View File

@ -0,0 +1,79 @@
<script lang="ts">
import activity, { ActivityMessage } from '@hcengineering/activity'
import ThreadParentMessage from './ThreadParentPresenter.svelte'
import { Label } from '@hcengineering/ui'
import ChannelScrollView from '../ChannelScrollView.svelte'
import { Ref } from '@hcengineering/core'
import { ChannelDataProvider } from '../../channelDataProvider'
import chunter from '../../plugin'
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let message: ActivityMessage
let dataProvider: ChannelDataProvider | undefined = undefined
$: if (message !== undefined && dataProvider === undefined) {
dataProvider = new ChannelDataProvider(
undefined,
message.space,
message._id,
chunter.class.ThreadMessage,
selectedMessageId,
true
)
}
$: messagesStore = dataProvider?.messagesStore
</script>
<div class="hulyComponent-content hulyComponent-content__container noShrink">
{#if dataProvider !== undefined}
<ChannelScrollView
bind:selectedMessageId
embedded
skipLabels
object={message}
provider={dataProvider}
fullHeight={false}
fixedInput={false}
>
<svelte:fragment slot="header">
<div class="mt-3">
<ThreadParentMessage {message} />
</div>
{#if (message.replies ?? $messagesStore?.length ?? 0) > 0}
<div class="separator">
<div class="label lower">
<Label
label={activity.string.RepliesCount}
params={{ replies: message.replies ?? $messagesStore?.length ?? 1 }}
/>
</div>
<div class="line" />
</div>
{/if}
</svelte:fragment>
</ChannelScrollView>
{/if}
</div>
<style lang="scss">
.separator {
display: flex;
align-items: center;
margin: 0.5rem 0;
.label {
white-space: nowrap;
margin: 0 0.5rem;
color: var(--theme-halfcontent-color);
}
.line {
background: var(--theme-refinput-border);
height: 1px;
width: 100%;
}
}
</style>

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import { Doc, Ref } from '@hcengineering/core' import { Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Breadcrumbs, Label, location as locationStore, Header, BreadcrumbItem } from '@hcengineering/ui' import { Breadcrumbs, location as locationStore, Header, BreadcrumbItem, Loading } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity' import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import { getMessageFromLoc, messageInFocus } from '@hcengineering/activity-resources' import { getMessageFromLoc, messageInFocus } from '@hcengineering/activity-resources'
@ -23,14 +23,14 @@
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import chunter from '../../plugin' import chunter from '../../plugin'
import ThreadParentMessage from './ThreadParentPresenter.svelte'
import { getObjectIcon, getChannelName } from '../../utils' import { getObjectIcon, getChannelName } from '../../utils'
import ChannelScrollView from '../ChannelScrollView.svelte' import { threadMessagesStore } from '../../stores'
import { ChannelDataProvider } from '../../channelDataProvider' import ThreadContent from './ThreadContent.svelte'
export let _id: Ref<ActivityMessage> export let _id: Ref<ActivityMessage>
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
export let showHeader: boolean = true export let showHeader: boolean = true
export let syncLocation = true
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
@ -40,12 +40,12 @@
const channelQuery = createQuery() const channelQuery = createQuery()
let channel: Doc | undefined = undefined let channel: Doc | undefined = undefined
let message: DisplayActivityMessage | undefined = undefined let message: DisplayActivityMessage | undefined = $threadMessagesStore?._id === _id ? $threadMessagesStore : undefined
let isLoading = true
let channelName: string | undefined = undefined let channelName: string | undefined = undefined
let dataProvider: ChannelDataProvider | undefined = undefined
const unsubscribe = messageInFocus.subscribe((id) => { const unsubscribe = messageInFocus.subscribe((id) => {
if (!syncLocation) return
if (id !== undefined && id !== selectedMessageId) { if (id !== undefined && id !== selectedMessageId) {
selectedMessageId = id selectedMessageId = id
} }
@ -54,6 +54,7 @@
}) })
const unsubscribeLocation = locationStore.subscribe((newLocation) => { const unsubscribeLocation = locationStore.subscribe((newLocation) => {
if (!syncLocation) return
const id = getMessageFromLoc(newLocation) const id = getMessageFromLoc(newLocation)
selectedMessageId = id selectedMessageId = id
messageInFocus.set(id) messageInFocus.set(id)
@ -64,12 +65,17 @@
unsubscribeLocation() unsubscribeLocation()
}) })
$: if (message && message._id !== _id) {
message = $threadMessagesStore?._id === _id ? $threadMessagesStore : undefined
isLoading = message === undefined
}
$: messageQuery.query( $: messageQuery.query(
activity.class.ActivityMessage, activity.class.ActivityMessage,
{ _id }, { _id },
(result: ActivityMessage[]) => { (result: ActivityMessage[]) => {
message = result[0] as DisplayActivityMessage message = result[0] as DisplayActivityMessage
isLoading = false
if (message === undefined) { if (message === undefined) {
dispatch('close') dispatch('close')
} }
@ -88,42 +94,26 @@
channel = res[0] channel = res[0]
}) })
$: if (message !== undefined && dataProvider === undefined) {
dataProvider = new ChannelDataProvider(
undefined,
message.space,
message._id,
chunter.class.ThreadMessage,
selectedMessageId,
true
)
}
$: message && $: message &&
getChannelName(message.attachedTo, message.attachedToClass, channel).then((res) => { getChannelName(message.attachedTo, message.attachedToClass, channel).then((res) => {
channelName = res channelName = res
}) })
let breadcrumbs: BreadcrumbItem[] = [] let breadcrumbs: BreadcrumbItem[] = []
$: breadcrumbs = showHeader ? getBreadcrumbsItems(channel, message, channelName) : [] $: breadcrumbs = showHeader ? getBreadcrumbsItems(channel, channelName) : []
function getBreadcrumbsItems ( function getBreadcrumbsItems (channel?: Doc, channelName?: string): BreadcrumbItem[] {
channel?: Doc, if (channel === undefined) {
message?: DisplayActivityMessage,
channelName?: string
): BreadcrumbItem[] {
if (message === undefined) {
return [] return []
} }
const isPersonAvatar = const isPersonAvatar =
message.attachedToClass === chunter.class.DirectMessage || channel._class === chunter.class.DirectMessage || hierarchy.isDerived(channel._class, contact.class.Person)
hierarchy.isDerived(message.attachedToClass, contact.class.Person)
return [ return [
{ {
id: 'channel', id: 'channel',
icon: getObjectIcon(message.attachedToClass), icon: getObjectIcon(channel._class),
iconProps: { value: channel }, iconProps: { value: channel },
iconWidth: isPersonAvatar ? 'auto' : undefined, iconWidth: isPersonAvatar ? 'auto' : undefined,
withoutIconBackground: isPersonAvatar, withoutIconBackground: isPersonAvatar,
@ -143,8 +133,6 @@
dispatch('channel') dispatch('channel')
} }
$: messagesStore = dataProvider?.messagesStore
</script> </script>
{#if showHeader} {#if showHeader}
@ -153,54 +141,10 @@
</Header> </Header>
{/if} {/if}
<div class="hulyComponent-content hulyComponent-content__container noShrink"> {#if message}
{#if message && dataProvider !== undefined} {#key _id}
<ChannelScrollView <ThreadContent bind:selectedMessageId {message} />
bind:selectedMessageId {/key}
embedded {:else if isLoading}
skipLabels <Loading />
object={message}
provider={dataProvider}
fullHeight={false}
fixedInput={false}
>
<svelte:fragment slot="header">
<div class="mt-3">
<ThreadParentMessage {message} />
</div>
{#if (message.replies ?? $messagesStore?.length ?? 0) > 0}
<div class="separator">
<div class="label lower">
<Label
label={activity.string.RepliesCount}
params={{ replies: message.replies ?? $messagesStore?.length ?? 1 }}
/>
</div>
<div class="line" />
</div>
{/if} {/if}
</svelte:fragment>
</ChannelScrollView>
{/if}
</div>
<style lang="scss">
.separator {
display: flex;
align-items: center;
margin: 0.5rem 0;
.label {
white-space: nowrap;
margin: 0 0.5rem;
color: var(--theme-halfcontent-color);
}
.line {
background: var(--theme-refinput-border);
height: 1px;
width: 100%;
}
}
</style>

View File

@ -28,6 +28,7 @@ import { get } from 'svelte/store'
import { chatSpecials } from './components/chat/utils' import { chatSpecials } from './components/chat/utils'
import { getChannelName, isThreadMessage } from './utils' import { getChannelName, isThreadMessage } from './utils'
import chunter from './plugin' import chunter from './plugin'
import { threadMessagesStore } from './stores'
export function openChannel (_id: string, _class: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>): void { export function openChannel (_id: string, _class: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>): void {
const loc = getCurrentLocation() const loc = getCurrentLocation()
@ -168,6 +169,8 @@ export async function replyToThread (message: ActivityMessage, e: Event): Promis
const fromSidebar = isElementFromSidebar(e.target as HTMLElement) const fromSidebar = isElementFromSidebar(e.target as HTMLElement)
const loc = getCurrentLocation() const loc = getCurrentLocation()
threadMessagesStore.set(message)
if (fromSidebar) { if (fromSidebar) {
const widget = getClient().getModel().findAllSync(workbench.class.Widget, { _id: chunter.ids.ChatWidget })[0] const widget = getClient().getModel().findAllSync(workbench.class.Widget, { _id: chunter.ids.ChatWidget })[0]
const widgetState = get(sidebarStore).widgetsState.get(widget._id) const widgetState = get(sidebarStore).widgetsState.get(widget._id)
@ -235,7 +238,8 @@ export async function openChannelInSidebar (
_class: Ref<Class<Doc>>, _class: Ref<Class<Doc>>,
doc?: Doc, doc?: Doc,
thread?: Ref<ActivityMessage>, thread?: Ref<ActivityMessage>,
newTab = true newTab = true,
selectedMessageId?: Ref<ActivityMessage>
): Promise<void> { ): Promise<void> {
const client = getClient() const client = getClient()
@ -267,6 +271,7 @@ export async function openChannelInSidebar (
_id, _id,
_class, _class,
thread, thread,
selectedMessageId,
channelName: name channelName: name
} }
} }

View File

@ -17,11 +17,14 @@ import { writable } from 'svelte/store'
import { type ChatMessage } from '@hcengineering/chunter' import { type ChatMessage } from '@hcengineering/chunter'
import { type Markup, type Ref } from '@hcengineering/core' import { type Markup, type Ref } from '@hcengineering/core'
import { languageStore } from '@hcengineering/ui' import { languageStore } from '@hcengineering/ui'
import { type ActivityMessage } from '@hcengineering/activity'
export const translatingMessagesStore = writable<Set<Ref<ChatMessage>>>(new Set()) export const translatingMessagesStore = writable<Set<Ref<ChatMessage>>>(new Set())
export const translatedMessagesStore = writable<Map<Ref<ChatMessage>, Markup>>(new Map()) export const translatedMessagesStore = writable<Map<Ref<ChatMessage>, Markup>>(new Map())
export const shownTranslatedMessagesStore = writable<Set<Ref<ChatMessage>>>(new Set()) export const shownTranslatedMessagesStore = writable<Set<Ref<ChatMessage>>>(new Set())
export const threadMessagesStore = writable<ActivityMessage | undefined>(undefined)
languageStore.subscribe(() => { languageStore.subscribe(() => {
translatedMessagesStore.set(new Map()) translatedMessagesStore.set(new Map())
shownTranslatedMessagesStore.set(new Set()) shownTranslatedMessagesStore.set(new Set())

View File

@ -100,7 +100,13 @@ export interface InlineButton extends AttachedDoc {
} }
export interface ChatWidgetTab extends WidgetTab { export interface ChatWidgetTab extends WidgetTab {
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>, channelName: string } data: {
_id?: Ref<Doc>
_class?: Ref<Class<Doc>>
thread?: Ref<ActivityMessage>
channelName: string
selectedMessageId?: Ref<ActivityMessage>
}
} }
/** /**
@ -234,7 +240,14 @@ export default plugin(chunterId, {
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>, CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
OpenThreadInSidebar: '' as Resource<(_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc) => Promise<void>>, OpenThreadInSidebar: '' as Resource<(_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc) => Promise<void>>,
OpenChannelInSidebar: '' as Resource< OpenChannelInSidebar: '' as Resource<
(_id: Ref<Doc>, _class: Ref<Doc>, doc?: Doc, thread?: Ref<ActivityMessage>) => Promise<void> (
_id: Ref<Doc>,
_class: Ref<Class<Doc>>,
doc?: Doc,
thread?: Ref<ActivityMessage>,
newTab?: boolean,
selectedMessageId?: Ref<ActivityMessage>
) => Promise<void>
> >
} }
}) })

View File

@ -64,6 +64,7 @@
$: if (object !== undefined && object?._id !== value.objectId) { $: if (object !== undefined && object?._id !== value.objectId) {
object = undefined object = undefined
isLoading = true
} }
let isActionMenuOpened = false let isActionMenuOpened = false

View File

@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { PersonAccount } from '@hcengineering/contact' import { PersonAccount } from '@hcengineering/contact'
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources' import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core' import { Class, Doc, Ref } from '@hcengineering/core'
import { BrowserNotification } from '@hcengineering/notification' import { BrowserNotification } from '@hcengineering/notification'
import { Button, navigate, Notification as PlatformNotification, NotificationToast } from '@hcengineering/ui' import { Button, navigate, Notification as PlatformNotification, NotificationToast } from '@hcengineering/ui'
import view, { decodeObjectURI } from '@hcengineering/view' import view from '@hcengineering/view'
import chunter from '@hcengineering/chunter' import chunter, { ThreadMessage } from '@hcengineering/chunter'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { ActivityMessage } from '@hcengineering/activity' import activity, { ActivityMessage } from '@hcengineering/activity'
import { getClient } from '@hcengineering/presentation'
import { pushAvailable, subscribePush } from '../utils' import { pushAvailable, subscribePush } from '../utils'
import plugin from '../plugin' import plugin from '../plugin'
@ -15,6 +16,9 @@
export let notification: PlatformNotification export let notification: PlatformNotification
export let onRemove: () => void export let onRemove: () => void
const client = getClient()
const hierarchy = client.getHierarchy()
$: value = notification.params?.value as BrowserNotification $: value = notification.params?.value as BrowserNotification
$: senderAccount = $: senderAccount =
@ -24,18 +28,35 @@
async function openChannelInSidebar (): Promise<void> { async function openChannelInSidebar (): Promise<void> {
if (!value.onClickLocation) return if (!value.onClickLocation) return
const { onClickLocation } = value const { onClickLocation } = value
const [_id, _class] = decodeObjectURI(onClickLocation.path[3] ?? '') let _id: Ref<Doc> | undefined = value.objectId
let _class: Ref<Class<Doc>> | undefined = value.objectClass
let thread = onClickLocation.path[4] as Ref<ActivityMessage> | undefined
const selectedMessageId: Ref<ActivityMessage> | undefined = value.messageId
if (_class && _id && hierarchy.isDerived(_class, activity.class.ActivityMessage)) {
const message = await client.findOne<ActivityMessage>(_class, { _id: _id as Ref<ActivityMessage> })
if (hierarchy.isDerived(_class, chunter.class.ThreadMessage)) {
const threadMessage = message as ThreadMessage
_id = threadMessage?.objectId
_class = threadMessage?.objectClass
thread = threadMessage?.attachedTo
} else {
_id = message?.attachedTo
_class = message?.attachedToClass
thread = (message?.replies ?? 0) > 0 ? message?._id : undefined
}
}
onRemove() onRemove()
if (!_id || !_class || _id === '' || _class === '') { if (!_id || !_class || _id === '' || _class === '' || selectedMessageId === undefined) {
navigate(onClickLocation) navigate(onClickLocation)
return return
} }
const thread = onClickLocation.path[4] as Ref<ActivityMessage> | undefined
const fn = await getResource(chunter.function.OpenChannelInSidebar) const fn = await getResource(chunter.function.OpenChannelInSidebar)
await fn(_id, _class, undefined, thread) await fn(_id, _class, undefined, thread, true, selectedMessageId)
} }
</script> </script>
@ -56,7 +77,7 @@
<Button <Button
label={view.string.Open} label={view.string.Open}
on:click={() => { on:click={() => {
openChannelInSidebar() void openChannelInSidebar()
}} }}
/> />
{/if} {/if}

View File

@ -53,6 +53,10 @@ export interface BrowserNotification extends Doc {
onClickLocation?: Location onClickLocation?: Location
senderId?: Ref<Account> senderId?: Ref<Account>
tag: Ref<Doc> tag: Ref<Doc>
messageId?: Ref<ActivityMessage>
messageClass?: Ref<Class<ActivityMessage>>
objectId: Ref<Doc>
objectClass: Ref<Class<Doc>>
} }
export interface PushData { export interface PushData {

View File

@ -0,0 +1,88 @@
//
// 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.
//
import { type Editor } from '@tiptap/core'
import ListKeymap, { type ListKeymapOptions, listHelpers } from '@tiptap/extension-list-keymap'
/**
* Workaround for the original ListKeymap extension issue that
* https://github.com/ueberdosis/tiptap/issues/4368
*/
export const ListKeymapExtension = ListKeymap.extend<ListKeymapOptions>({
addKeyboardShortcuts () {
const handleBackspace = (editor: Editor): boolean => {
let handled = false
if (!editor.state.selection.empty) {
return false
}
this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
if (editor.state.schema.nodes[itemName] === undefined) {
return
}
if (listHelpers.handleBackspace(editor, itemName, wrapperNames)) {
handled = true
}
})
return handled
}
const handleDelete = (editor: Editor): boolean => {
let handled = false
if (!editor.state.selection.empty) {
return false
}
this.options.listTypes.forEach(({ itemName }) => {
if (editor.state.schema.nodes[itemName] === undefined) {
return
}
if (listHelpers.handleDelete(editor, itemName)) {
handled = true
}
})
return handled
}
const handleBackspaceSafe = (editor: Editor): boolean => {
try {
return handleBackspace(editor)
} catch (e) {
console.log(e)
return false
}
}
const handleDeleteSafe = (editor: Editor): boolean => {
try {
return handleDelete(editor)
} catch (e) {
console.log(e)
return false
}
}
return {
Backspace: ({ editor }) => handleBackspaceSafe(editor),
'Mod-Backspace': ({ editor }) => handleBackspaceSafe(editor),
Delete: ({ editor }) => handleDeleteSafe(editor),
'Mod-Delete': ({ editor }) => handleDeleteSafe(editor)
}
}
})

View File

@ -19,7 +19,6 @@ import { CodeExtension, codeOptions } from '@hcengineering/text'
import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor' import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor'
import { type AnyExtension, type Editor, Extension } from '@tiptap/core' import { type AnyExtension, type Editor, Extension } from '@tiptap/core'
import { type Level } from '@tiptap/extension-heading' import { type Level } from '@tiptap/extension-heading'
import ListKeymap from '@tiptap/extension-list-keymap'
import TableHeader from '@tiptap/extension-table-header' import TableHeader from '@tiptap/extension-table-header'
import 'prosemirror-codemark/dist/codemark.css' import 'prosemirror-codemark/dist/codemark.css'
@ -30,6 +29,7 @@ import { FileExtension, type FileOptions } from '../components/extension/fileExt
import { HardBreakExtension } from '../components/extension/hardBreak' import { HardBreakExtension } from '../components/extension/hardBreak'
import { ImageExtension, type ImageOptions } from '../components/extension/imageExt' import { ImageExtension, type ImageOptions } from '../components/extension/imageExt'
import { InlineToolbarExtension } from '../components/extension/inlineToolbar' import { InlineToolbarExtension } from '../components/extension/inlineToolbar'
import { ListKeymapExtension } from '../components/extension/listkeymap'
import { NodeUuidExtension } from '../components/extension/nodeUuid' import { NodeUuidExtension } from '../components/extension/nodeUuid'
import { ParagraphExtension } from '../components/extension/paragraph' import { ParagraphExtension } from '../components/extension/paragraph'
import { SubmitExtension, type SubmitOptions } from '../components/extension/submit' import { SubmitExtension, type SubmitOptions } from '../components/extension/submit'
@ -195,7 +195,7 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
staticKitExtensions.push([ staticKitExtensions.push([
500, 500,
ListKeymap.configure({ ListKeymapExtension.configure({
listTypes: [ listTypes: [
{ {
itemName: 'listItem', itemName: 'listItem',

View File

@ -74,17 +74,16 @@
on:change={(e) => change(e, slot)} on:change={(e) => change(e, slot)}
on:dueChange={(e) => dueChange(e, slot)} on:dueChange={(e) => dueChange(e, slot)}
/> />
<div class="tool flex-no-shrink">
<ButtonIcon <ButtonIcon
kind="tertiary" kind="tertiary"
size="small" size="small"
icon={IconDelete} icon={IconDelete}
dataId={'btnDelete'}
on:click={() => { on:click={() => {
dispatch('remove', { _id: slot._id }) dispatch('remove', { _id: slot._id })
}} }}
/> />
</div> </div>
</div>
{/each} {/each}
</Scroller> </Scroller>
<div class="flex-row-center flex-gap-4"> <div class="flex-row-center flex-gap-4">

View File

@ -160,6 +160,7 @@ export default plugin(timeId, {
CreatedToDo: '' as IntlString, CreatedToDo: '' as IntlString,
AddToDo: '' as IntlString, AddToDo: '' as IntlString,
NewToDoDetails: '' as IntlString, NewToDoDetails: '' as IntlString,
ToDo: '' as IntlString ToDo: '' as IntlString,
NewToDo: '' as IntlString
} }
}) })

View File

@ -316,3 +316,20 @@ export function minimizeSidebar (closedByUser = false): void {
sidebarStore.set({ ...state, ...widgetsState, widget: undefined, variant: SidebarVariant.MINI }) sidebarStore.set({ ...state, ...widgetsState, widget: undefined, variant: SidebarVariant.MINI })
} }
export function updateTabData (widget: Ref<Widget>, tabId: string, data: Record<string, any>): void {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget)
if (widgetState === undefined) return
const tabs = widgetState.tabs.map((it) => (it.id === tabId ? { ...it, data: { ...it.data, ...data } } : it))
widgetsState.set(widget, { ...widgetState, tabs })
sidebarStore.set({
...state,
widgetsState
})
}

View File

@ -175,7 +175,8 @@ export async function getCommonNotificationTxes (
doc, doc,
receiver, receiver,
sender, sender,
subscriptions subscriptions,
_class
) )
} }
@ -487,6 +488,14 @@ export async function getTranslatedNotificationContent (
return { title: '', body: '' } return { title: '', body: '' }
} }
function isReactionMessage (message?: ActivityMessage): boolean {
return (
message !== undefined &&
message._class === activity.class.DocUpdateMessage &&
(message as DocUpdateMessage).objectClass === activity.class.Reaction
)
}
export async function createPushFromInbox ( export async function createPushFromInbox (
control: TriggerControl, control: TriggerControl,
receiver: ReceiverInfo, receiver: ReceiverInfo,
@ -497,10 +506,9 @@ export async function createPushFromInbox (
sender: SenderInfo, sender: SenderInfo,
_id: Ref<Doc>, _id: Ref<Doc>,
subscriptions: PushSubscription[], subscriptions: PushSubscription[],
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>() message?: ActivityMessage
): Promise<Tx | undefined> { ): Promise<Tx | undefined> {
let { title, body } = await getTranslatedNotificationContent(data, _class, control) let { title, body } = await getTranslatedNotificationContent(data, _class, control)
if (title === '' || body === '') { if (title === '' || body === '') {
return return
} }
@ -515,13 +523,12 @@ export async function createPushFromInbox (
if (provider !== undefined) { if (provider !== undefined) {
const encodeFn = await getResource(provider.encode) const encodeFn = await getResource(provider.encode)
const doc = cache.get(attachedTo) ?? (await control.findAll(control.ctx, attachedToClass, { _id: attachedTo }))[0] const doc = (await control.findAll(control.ctx, attachedToClass, { _id: attachedTo }))[0]
if (doc === undefined) { if (doc === undefined) {
return return
} }
cache.set(doc._id, doc)
id = await encodeFn(doc, control) id = await encodeFn(doc, control)
} }
@ -543,6 +550,12 @@ export async function createPushFromInbox (
body, body,
senderId: sender._id, senderId: sender._id,
tag: _id, tag: _id,
objectId: attachedTo,
objectClass: attachedToClass,
messageId: isReactionMessage(message) ? (message?.attachedTo as Ref<ActivityMessage>) : message?._id,
messageClass: isReactionMessage(message)
? (message?.attachedToClass as Ref<Class<ActivityMessage>>)
: message?._class,
onClickLocation: { onClickLocation: {
path path
} }
@ -664,6 +677,7 @@ export async function applyNotificationProviders (
receiver: ReceiverInfo, receiver: ReceiverInfo,
sender: SenderInfo, sender: SenderInfo,
subscriptions: PushSubscription[], subscriptions: PushSubscription[],
_class = notification.class.ActivityInboxNotification,
message?: ActivityMessage message?: ActivityMessage
): Promise<void> { ): Promise<void> {
const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {}) const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {})
@ -676,10 +690,11 @@ export async function applyNotificationProviders (
attachedTo, attachedTo,
attachedToClass, attachedToClass,
data, data,
notification.class.ActivityInboxNotification, _class,
sender, sender,
data._id, data._id,
subscriptions subscriptions,
message
) )
if (pushTx !== undefined) { if (pushTx !== undefined) {
res.push(pushTx) res.push(pushTx)
@ -796,6 +811,7 @@ export async function getNotificationTxes (
receiver, receiver,
sender, sender,
subscriptions, subscriptions,
notificationData._class,
message message
) )
} }

View File

@ -172,6 +172,8 @@
"@hcengineering/github": "^0.6.0", "@hcengineering/github": "^0.6.0",
"@hcengineering/github-assets": "^0.6.0", "@hcengineering/github-assets": "^0.6.0",
"@hcengineering/server-ai-bot": "^0.6.0", "@hcengineering/server-ai-bot": "^0.6.0",
"@hcengineering/server-ai-bot-resources": "^0.6.0" "@hcengineering/server-ai-bot-resources": "^0.6.0",
"@hcengineering/time": "^0.6.0",
"@hcengineering/time-assets": "^0.6.0"
} }
} }

View File

@ -33,6 +33,7 @@ import { trackerId } from '@hcengineering/tracker'
import { trainingId } from '@hcengineering/training' import { trainingId } from '@hcengineering/training'
import { viewId } from '@hcengineering/view' import { viewId } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import { timeId } from '@hcengineering/time'
import coreEng from '@hcengineering/core/lang/en.json' import coreEng from '@hcengineering/core/lang/en.json'
import loginEng from '@hcengineering/login-assets/lang/en.json' import loginEng from '@hcengineering/login-assets/lang/en.json'
import platformEng from '@hcengineering/platform/lang/en.json' import platformEng from '@hcengineering/platform/lang/en.json'
@ -68,6 +69,7 @@ import trackerEn from '@hcengineering/tracker-assets/lang/en.json'
import trainingEn from '@hcengineering/training-assets/lang/en.json' import trainingEn from '@hcengineering/training-assets/lang/en.json'
import viewEn from '@hcengineering/view-assets/lang/en.json' import viewEn from '@hcengineering/view-assets/lang/en.json'
import workbenchEn from '@hcengineering/workbench-assets/lang/en.json' import workbenchEn from '@hcengineering/workbench-assets/lang/en.json'
import timeEn from '@hcengineering/time-assets/lang/en.json'
export function registerStringLoaders (): void { export function registerStringLoaders (): void {
addStringsLoader(coreId, async (lang: string) => coreEng) addStringsLoader(coreId, async (lang: string) => coreEng)
@ -106,4 +108,5 @@ export function registerStringLoaders (): void {
addStringsLoader(productsId, async (lang: string) => productsEn) addStringsLoader(productsId, async (lang: string) => productsEn)
addStringsLoader(trainingId, async (lang: string) => trainingEn) addStringsLoader(trainingId, async (lang: string) => trainingEn)
addStringsLoader(githubId, async (lang: string) => githubEn) addStringsLoader(githubId, async (lang: string) => githubEn)
addStringsLoader(timeId, async (lang: string) => timeEn)
} }

View File

@ -24,10 +24,10 @@ export async function onAuthorize (login?: string): Promise<void> {
) )
const client = getClient() const client = getClient()
const config = await client.findOne(github.class.GithubAuthentication, {}) // Remove old authorizations.
const config = await client.findAll(github.class.GithubAuthentication, {})
if (config !== undefined) { for (const c of config) {
await client.remove(config) await client.remove(c)
} }
await client.createDoc<GithubAuthentication>(github.class.GithubAuthentication, core.space.Workspace, { await client.createDoc<GithubAuthentication>(github.class.GithubAuthentication, core.space.Workspace, {
attachedTo: meId, attachedTo: meId,

View File

@ -59,7 +59,7 @@ export class PlanningPage extends CalendarPage {
readonly textPanelToDoDescription = (): Locator => this.panel().locator('div.top-content div.tiptap > p') readonly textPanelToDoDescription = (): Locator => this.panel().locator('div.top-content div.tiptap > p')
readonly textPanelDueDate = (): Locator => readonly textPanelDueDate = (): Locator =>
this.panel().locator( this.panel().locator(
'div.slots-content div.flex-row-top.justify-between div.flex-row-center button:first-child span' 'div.slots-content div.flex-row-top.justify-between div.flex-row-center .hulyButton:first-child span'
) )
readonly textPanelPriority = (): Locator => this.panel().locator('button#priorityButton svg') readonly textPanelPriority = (): Locator => this.panel().locator('button#priorityButton svg')
@ -189,7 +189,7 @@ export class PlanningPage extends CalendarPage {
const row = this.page.locator(p).nth(rowNumber) const row = this.page.locator(p).nth(rowNumber)
// dateStart // dateStart
await row.locator('div.dateEditor-container:first-child > div.min-w-28:first-child button').click() await row.locator('div.dateEditor-container:first-child > div.min-w-28:first-child .hulyButton').click()
if (slot.dateStart === 'today') { if (slot.dateStart === 'today') {
await this.buttonCalendarToday().click() await this.buttonCalendarToday().click()
} else { } else {
@ -204,21 +204,17 @@ export class PlanningPage extends CalendarPage {
// timeStart // timeStart
const hours = slot.timeStart.substring(0, 2) const hours = slot.timeStart.substring(0, 2)
const minutes = slot.timeStart.substring(2, slot.timeStart.length) const minutes = slot.timeStart.substring(2, slot.timeStart.length)
await row.locator('div.dateEditor-container:nth-child(1) .hulyButton span.digit:first-child').focus()
await row await row
.locator('div.dateEditor-container:nth-child(1) button:last-child span.digit:first-child') .locator('div.dateEditor-container:nth-child(1) .hulyButton span.digit:first-child')
.click({ delay: 200 })
await row
.locator('div.dateEditor-container:nth-child(1) button:last-child span.digit:first-child')
.pressSequentially(hours, { delay: 100 }) .pressSequentially(hours, { delay: 100 })
await row.locator('div.dateEditor-container:nth-child(1) .hulyButton span.digit:last-child').focus()
await row await row
.locator('div.dateEditor-container:nth-child(1) button:last-child span.digit:last-child') .locator('div.dateEditor-container:nth-child(1) .hulyButton span.digit:last-child')
.click({ delay: 200 })
await row
.locator('div.dateEditor-container:nth-child(1) button:last-child span.digit:last-child')
.pressSequentially(minutes, { delay: 100 }) .pressSequentially(minutes, { delay: 100 })
// dateEnd + timeEnd // dateEnd + timeEnd
await row.locator('div.dateEditor-container.difference button').click() await row.locator('div.dateEditor-container.difference .hulyButton').click()
await this.fillSelectDatePopup(slot.dateEnd.day, slot.dateEnd.month, slot.dateEnd.year, slot.timeEnd) await this.fillSelectDatePopup(slot.dateEnd.day, slot.dateEnd.month, slot.dateEnd.year, slot.timeEnd)
} }
@ -228,11 +224,13 @@ export class PlanningPage extends CalendarPage {
: 'div.hulyModal-container div.slots-content div.scroller-container div.box div.flex-between.min-w-full' : 'div.hulyModal-container div.slots-content div.scroller-container div.box div.flex-between.min-w-full'
const row = this.page.locator(p).nth(rowNumber) const row = this.page.locator(p).nth(rowNumber)
// timeStart // timeStart
await expect(row.locator('div.dateEditor-container:nth-child(1) button:last-child div.datetime-input')).toHaveText( await expect(
slot.timeStart row.locator('div.dateEditor-container:nth-child(1) .hulyButton:last-child div.datetime-input')
) ).toHaveText(slot.timeStart)
// timeEnd // timeEnd
await expect(row.locator('div.dateEditor-container.difference button > div:first-child')).toHaveText(slot.timeEnd) await expect(row.locator('div.dateEditor-container.difference .hulyButton > div:first-child')).toHaveText(
slot.timeEnd
)
} }
async openToDoByName (toDoName: string): Promise<void> { async openToDoByName (toDoName: string): Promise<void> {
@ -304,11 +302,10 @@ export class PlanningPage extends CalendarPage {
public async deleteTimeSlot (rowNumber: number): Promise<void> { public async deleteTimeSlot (rowNumber: number): Promise<void> {
const row = this.page const row = this.page
.locator( .locator(
'div.hulyModal-container div.slots-content div.scroller-container div.box div.flex-between.min-w-full div.tool' 'div.hulyModal-container div.slots-content div.scroller-container div.box div.flex-between.min-w-full button[data-id="btnDelete"]'
) )
.nth(rowNumber) .nth(rowNumber)
await row.locator('xpath=..').hover() await row.click()
await row.locator('button').click()
await this.pressYesDeletePopup(this.page) await this.pressYesDeletePopup(this.page)
} }
@ -317,8 +314,8 @@ export class PlanningPage extends CalendarPage {
.locator('div.hulyModal-container div.slots-content div.scroller-container div.box div.flex-between.min-w-full') .locator('div.hulyModal-container div.slots-content div.scroller-container div.box div.flex-between.min-w-full')
.nth(rowNumber) .nth(rowNumber)
// dateEnd // dateEnd
await expect(row.locator('div.dateEditor-container:first-child > div.min-w-28:first-child button')).toContainText( await expect(
dateEnd row.locator('div.dateEditor-container:first-child > div.min-w-28:first-child .hulyButton')
) ).toContainText(dateEnd)
} }
} }