mirror of
https://github.com/hcengineering/platform.git
synced 2025-03-19 13:18:56 +00:00
Merge remote-tracking branch 'origin/develop' into staging
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
commit
a72df539e2
@ -1691,6 +1691,9 @@ dependencies:
|
|||||||
openai:
|
openai:
|
||||||
specifier: ^4.56.0
|
specifier: ^4.56.0
|
||||||
version: 4.56.0(zod@3.23.8)
|
version: 4.56.0(zod@3.23.8)
|
||||||
|
openid-client:
|
||||||
|
specifier: ~5.7.0
|
||||||
|
version: 5.7.0
|
||||||
otp-generator:
|
otp-generator:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@ -17193,6 +17196,10 @@ packages:
|
|||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/jose@4.15.9:
|
||||||
|
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/jose@5.6.3:
|
/jose@5.6.3:
|
||||||
resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==}
|
resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -19076,6 +19083,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ==}
|
resolution: {integrity: sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/object-hash@2.2.0:
|
||||||
|
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/object-hash@3.0.0:
|
/object-hash@3.0.0:
|
||||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -19210,6 +19222,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==}
|
resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/oidc-token-hash@5.0.3:
|
||||||
|
resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==}
|
||||||
|
engines: {node: ^10.13.0 || >=12.0.0}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/omggif@1.0.10:
|
/omggif@1.0.10:
|
||||||
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
|
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -19298,6 +19315,15 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/openid-client@5.7.0:
|
||||||
|
resolution: {integrity: sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==}
|
||||||
|
dependencies:
|
||||||
|
jose: 4.15.9
|
||||||
|
lru-cache: 6.0.0
|
||||||
|
object-hash: 2.2.0
|
||||||
|
oidc-token-hash: 5.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/option@0.2.4:
|
/option@0.2.4:
|
||||||
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
|
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -25220,7 +25246,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/auth-providers.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
|
file:projects/auth-providers.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
|
||||||
resolution: {integrity: sha512-RDD+zkNosRxJuY+bq2skRGbRPr4saOaAvlkKv3gvvIXT5RxnmOXnznS6haxIcXw4VA96oxv08U9Mu8+3PjgpwQ==, tarball: file:projects/auth-providers.tgz}
|
resolution: {integrity: sha512-0YbyLxnpaSZfdCfdlt5T9LI/TmahO7fbLohsHvXQVFb2J1InAIyu1WWzbS92CLYLHxQtJmSIOAbPVbRV1wPWbw==, tarball: file:projects/auth-providers.tgz}
|
||||||
id: file:projects/auth-providers.tgz
|
id: file:projects/auth-providers.tgz
|
||||||
name: '@rush-temp/auth-providers'
|
name: '@rush-temp/auth-providers'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
@ -25248,6 +25274,7 @@ packages:
|
|||||||
koa-router: 12.0.1
|
koa-router: 12.0.1
|
||||||
koa-session: 6.4.0
|
koa-session: 6.4.0
|
||||||
mongodb: 6.9.0
|
mongodb: 6.9.0
|
||||||
|
openid-client: 5.7.0
|
||||||
passport-custom: 1.1.1
|
passport-custom: 1.1.1
|
||||||
passport-github2: 0.1.12
|
passport-github2: 0.1.12
|
||||||
passport-google-oauth20: 2.0.0
|
passport-google-oauth20: 2.0.0
|
||||||
|
@ -152,6 +152,9 @@ export class TActivityReference extends TActivityMessage implements ActivityRefe
|
|||||||
@Prop(TypeMarkup(), activity.string.Message)
|
@Prop(TypeMarkup(), activity.string.Message)
|
||||||
@Index(IndexKind.FullText)
|
@Index(IndexKind.FullText)
|
||||||
message!: string
|
message!: string
|
||||||
|
|
||||||
|
@Prop(TypeTimestamp(), activity.string.Edit)
|
||||||
|
editedOn?: Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
@Model(activity.class.ActivityInfoMessage, activity.class.ActivityMessage)
|
@Model(activity.class.ActivityInfoMessage, activity.class.ActivityMessage)
|
||||||
|
@ -22,7 +22,6 @@ import {
|
|||||||
TypeMarkup,
|
TypeMarkup,
|
||||||
TypeRef,
|
TypeRef,
|
||||||
TypeString,
|
TypeString,
|
||||||
TypeTimestamp,
|
|
||||||
UX
|
UX
|
||||||
} from '@hcengineering/model'
|
} from '@hcengineering/model'
|
||||||
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
||||||
@ -59,6 +58,7 @@ import type { DocNotifyContext } from '@hcengineering/notification'
|
|||||||
import chunter from './plugin'
|
import chunter from './plugin'
|
||||||
|
|
||||||
export const DOMAIN_CHUNTER = 'chunter' as Domain
|
export const DOMAIN_CHUNTER = 'chunter' as Domain
|
||||||
|
|
||||||
@Model(chunter.class.ChunterSpace, core.class.Space)
|
@Model(chunter.class.ChunterSpace, core.class.Space)
|
||||||
export class TChunterSpace extends TSpace implements ChunterSpace {
|
export class TChunterSpace extends TSpace implements ChunterSpace {
|
||||||
@Prop(PropCollection(activity.class.ActivityMessage), chunter.string.Messages)
|
@Prop(PropCollection(activity.class.ActivityMessage), chunter.string.Messages)
|
||||||
@ -84,9 +84,6 @@ export class TChatMessage extends TActivityMessage implements ChatMessage {
|
|||||||
@Index(IndexKind.FullText)
|
@Index(IndexKind.FullText)
|
||||||
message!: string
|
message!: string
|
||||||
|
|
||||||
@Prop(TypeTimestamp(), chunter.string.Edit)
|
|
||||||
editedOn?: Timestamp
|
|
||||||
|
|
||||||
@Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, {
|
@Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, {
|
||||||
shortLabel: attachment.string.Files
|
shortLabel: attachment.string.Files
|
||||||
})
|
})
|
||||||
|
@ -562,6 +562,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
|||||||
input: 'focus',
|
input: 'focus',
|
||||||
category: tracker.category.Tracker,
|
category: tracker.category.Tracker,
|
||||||
target: tracker.class.Issue,
|
target: tracker.class.Issue,
|
||||||
|
visibilityTester: view.function.IsClipboardAvailable,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser'],
|
mode: ['context', 'browser'],
|
||||||
application: tracker.app.Tracker,
|
application: tracker.app.Tracker,
|
||||||
@ -582,6 +583,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
|||||||
input: 'focus',
|
input: 'focus',
|
||||||
category: tracker.category.Tracker,
|
category: tracker.category.Tracker,
|
||||||
target: tracker.class.Issue,
|
target: tracker.class.Issue,
|
||||||
|
visibilityTester: view.function.IsClipboardAvailable,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser'],
|
mode: ['context', 'browser'],
|
||||||
application: tracker.app.Tracker,
|
application: tracker.app.Tracker,
|
||||||
@ -602,6 +604,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
|||||||
input: 'focus',
|
input: 'focus',
|
||||||
category: tracker.category.Tracker,
|
category: tracker.category.Tracker,
|
||||||
target: tracker.class.Issue,
|
target: tracker.class.Issue,
|
||||||
|
visibilityTester: view.function.IsClipboardAvailable,
|
||||||
context: {
|
context: {
|
||||||
mode: ['context', 'browser'],
|
mode: ['context', 'browser'],
|
||||||
application: tracker.app.Tracker,
|
application: tracker.app.Tracker,
|
||||||
|
@ -140,6 +140,7 @@ export default mergeIds(viewId, view, {
|
|||||||
CanDeleteSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
CanDeleteSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
CanJoinSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
CanJoinSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
CanLeaveSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
CanLeaveSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
|
IsClipboardAvailable: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||||
BlobImageMetadata: '' as Resource<(file: FileOrBlob, blob: Ref<Blob>) => Promise<BlobMetadata | undefined>>,
|
BlobImageMetadata: '' as Resource<(file: FileOrBlob, blob: Ref<Blob>) => Promise<BlobMetadata | undefined>>,
|
||||||
BlobVideoMetadata: '' as Resource<(file: FileOrBlob, blob: Ref<Blob>) => Promise<BlobMetadata | undefined>>
|
BlobVideoMetadata: '' as Resource<(file: FileOrBlob, blob: Ref<Blob>) => Promise<BlobMetadata | undefined>>
|
||||||
},
|
},
|
||||||
|
@ -321,6 +321,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: var(--spacing-6_5);
|
min-height: var(--spacing-6_5);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&.clearPadding > .hulyHeader-row {
|
&.clearPadding > .hulyHeader-row {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -577,9 +578,12 @@
|
|||||||
}
|
}
|
||||||
&.second:not(.isOpen),
|
&.second:not(.isOpen),
|
||||||
&.border,
|
&.border,
|
||||||
&.default:not(.nested:last-child) {
|
&.default:not(.nested) {
|
||||||
border-bottom: 1px solid var(--theme-navpanel-divider); // var(--global-surface-01-BorderColor);
|
border-bottom: 1px solid var(--theme-navpanel-divider); // var(--global-surface-01-BorderColor);
|
||||||
}
|
}
|
||||||
|
&.default.nested:not(:last-child) {
|
||||||
|
border-bottom: 1px dashed var(--theme-navpanel-divider);
|
||||||
|
}
|
||||||
.hulyAccordionItem-header {
|
.hulyAccordionItem-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -790,6 +794,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.hiddenHeader {
|
||||||
|
display: none;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&.nested + .hulyAccordionItem-content { padding-top: var(--spacing-1); }
|
||||||
|
}
|
||||||
&:hover .hulyAccordionItem-header__chevron {
|
&:hover .hulyAccordionItem-header__chevron {
|
||||||
color: var(--button-subtle-IconColor);
|
color: var(--button-subtle-IconColor);
|
||||||
background-color: var(--global-ui-hover-BackgroundColor);
|
background-color: var(--global-ui-hover-BackgroundColor);
|
||||||
@ -806,6 +816,9 @@
|
|||||||
&.medium.bottomSpace + .hulyAccordionItem-content {
|
&.medium.bottomSpace + .hulyAccordionItem-content {
|
||||||
padding-bottom: var(--spacing-2);
|
padding-bottom: var(--spacing-2);
|
||||||
}
|
}
|
||||||
|
&.medium.nested.bottomSpace + .hulyAccordionItem-content {
|
||||||
|
padding-bottom: var(--spacing-1);
|
||||||
|
}
|
||||||
&.large.bottomSpace + .hulyAccordionItem-content {
|
&.large.bottomSpace + .hulyAccordionItem-content {
|
||||||
padding-bottom: var(--spacing-2);
|
padding-bottom: var(--spacing-2);
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
export let duration: number | boolean = false
|
export let duration: number | boolean = false
|
||||||
export let fixHeader: boolean = false
|
export let fixHeader: boolean = false
|
||||||
export let categoryHeader: boolean = false
|
export let categoryHeader: boolean = false
|
||||||
|
export let hiddenHeader: boolean = false
|
||||||
export let background: string | undefined = undefined
|
export let background: string | undefined = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
@ -74,6 +75,7 @@
|
|||||||
class:selectable
|
class:selectable
|
||||||
class:scroller-header={fixHeader}
|
class:scroller-header={fixHeader}
|
||||||
class:categoryHeader
|
class:categoryHeader
|
||||||
|
class:hiddenHeader
|
||||||
style:background-color={background ?? 'transparent'}
|
style:background-color={background ?? 'transparent'}
|
||||||
on:click|stopPropagation={handleClick}
|
on:click|stopPropagation={handleClick}
|
||||||
>
|
>
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
export let noPrint: boolean = false
|
export let noPrint: boolean = false
|
||||||
export let freezeBefore: boolean = false
|
export let freezeBefore: boolean = false
|
||||||
export let doubleRowWidth = 768
|
export let doubleRowWidth = 768
|
||||||
|
export let closeOnEscape: boolean = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
@ -61,7 +62,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
function _close (ev: KeyboardEvent): void {
|
function _close (ev: KeyboardEvent): void {
|
||||||
if (closeButton && ev.key === 'Escape') {
|
if (closeButton && ev.key === 'Escape' && closeOnEscape) {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
|
|
||||||
@ -138,7 +139,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if closeButton}
|
{#if closeButton}
|
||||||
{#if type !== 'type-popup'}<div class="hulyHeader-divider no-print" />{/if}
|
{#if type !== 'type-popup'}<div class="hulyHeader-divider no-print" />{/if}
|
||||||
|
{#if closeOnEscape}
|
||||||
<div class="hulyHotKey-item no-print">Esc</div>
|
<div class="hulyHotKey-item no-print">Esc</div>
|
||||||
|
{/if}
|
||||||
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} noPrint on:click={() => dispatch('close')} />
|
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} noPrint on:click={() => dispatch('close')} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -233,7 +236,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if closeButton}
|
{#if closeButton}
|
||||||
{#if type !== 'type-popup'}<div class="hulyHeader-divider no-print" />{/if}
|
{#if type !== 'type-popup'}<div class="hulyHeader-divider no-print" />{/if}
|
||||||
|
{#if closeOnEscape}
|
||||||
<div class="hulyHotKey-item no-print">Esc</div>
|
<div class="hulyHotKey-item no-print">Esc</div>
|
||||||
|
{/if}
|
||||||
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} noPrint on:click={() => dispatch('close')} />
|
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} noPrint on:click={() => dispatch('close')} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -85,6 +85,7 @@
|
|||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: var(--theme-content-color);
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: var(--theme-button-pressed);
|
background-color: var(--theme-button-pressed);
|
||||||
|
@ -126,6 +126,7 @@ export interface TabItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BreadcrumbItem {
|
export interface BreadcrumbItem {
|
||||||
|
id?: string
|
||||||
icon?: Asset | AnySvelteComponent | ComponentType
|
icon?: Asset | AnySvelteComponent | ComponentType
|
||||||
iconProps?: any
|
iconProps?: any
|
||||||
iconWidth?: string
|
iconWidth?: string
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
|
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
|
||||||
import { Asset } from '@hcengineering/platform'
|
import { Asset } from '@hcengineering/platform'
|
||||||
import { Action as ViewAction } from '@hcengineering/view'
|
import { Action as ViewAction } from '@hcengineering/view'
|
||||||
|
import notification from '@hcengineering/notification'
|
||||||
|
|
||||||
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
|
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
|
||||||
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
|
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
|
||||||
@ -228,6 +229,9 @@
|
|||||||
<span class="text-sm lower">
|
<span class="text-sm lower">
|
||||||
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
|
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
|
||||||
</span>
|
</span>
|
||||||
|
{#if message.editedOn}
|
||||||
|
<span class="text-sm lower">(<Label label={notification.string.Edited} />)</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if withActions && inlineActions.length > 0 && !readonly}
|
{#if withActions && inlineActions.length > 0 && !readonly}
|
||||||
<div class="flex-presenter flex-gap-2 ml-2">
|
<div class="flex-presenter flex-gap-2 ml-2">
|
||||||
|
@ -46,6 +46,7 @@ export interface ActivityMessage extends AttachedDoc {
|
|||||||
|
|
||||||
replies?: number
|
replies?: number
|
||||||
reactions?: number
|
reactions?: number
|
||||||
|
editedOn?: Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DisplayActivityMessage = DisplayDocUpdateMessage | ActivityMessage
|
export type DisplayActivityMessage = DisplayDocUpdateMessage | ActivityMessage
|
||||||
@ -81,6 +82,7 @@ export interface ActivityInfoMessage extends ActivityMessage {
|
|||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
icon?: Asset
|
icon?: Asset
|
||||||
iconProps?: Record<string, any>
|
iconProps?: Record<string, any>
|
||||||
|
editedOn?: Timestamp
|
||||||
|
|
||||||
// A possible set of links to some platform resources.
|
// A possible set of links to some platform resources.
|
||||||
links?: { _class: Ref<Class<Doc>>, _id: Ref<Doc> }[]
|
links?: { _class: Ref<Class<Doc>>, _id: Ref<Doc> }[]
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
hideExtra={false}
|
hideExtra={false}
|
||||||
adaptive="autoExtra"
|
adaptive="autoExtra"
|
||||||
doubleRowWidth={350}
|
doubleRowWidth={350}
|
||||||
|
closeOnEscape={false}
|
||||||
on:close
|
on:close
|
||||||
>
|
>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
export let filters: Ref<ActivityMessagesFilter>[] = []
|
export let filters: Ref<ActivityMessagesFilter>[] = []
|
||||||
export let isAsideOpened = false
|
export let isAsideOpened = false
|
||||||
export let syncLocation = true
|
export let syncLocation = true
|
||||||
|
export let freeze = false
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -111,5 +112,6 @@
|
|||||||
provider={dataProvider}
|
provider={dataProvider}
|
||||||
{isAsideOpened}
|
{isAsideOpened}
|
||||||
loadMoreAllowed={!isDocChannel}
|
loadMoreAllowed={!isDocChannel}
|
||||||
|
{freeze}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
export let isAsideShown: boolean = false
|
export let isAsideShown: boolean = false
|
||||||
export let filters: Ref<ActivityMessagesFilter>[] = []
|
export let filters: Ref<ActivityMessagesFilter>[] = []
|
||||||
export let canOpenInSidebar: boolean = false
|
export let canOpenInSidebar: boolean = false
|
||||||
|
export let closeOnEscape: boolean = true
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -80,6 +81,7 @@
|
|||||||
{isAsideShown}
|
{isAsideShown}
|
||||||
{withSearch}
|
{withSearch}
|
||||||
{canOpenInSidebar}
|
{canOpenInSidebar}
|
||||||
|
{closeOnEscape}
|
||||||
on:aside-toggled
|
on:aside-toggled
|
||||||
on:close
|
on:close
|
||||||
>
|
>
|
||||||
|
@ -61,8 +61,9 @@
|
|||||||
export let skipLabels = false
|
export let skipLabels = false
|
||||||
export let loadMoreAllowed = true
|
export let loadMoreAllowed = true
|
||||||
export let isAsideOpened = false
|
export let isAsideOpened = false
|
||||||
export let initialScrollBottom = true
|
|
||||||
export let fullHeight = true
|
export let fullHeight = true
|
||||||
|
export let fixedInput = true
|
||||||
|
export let freeze = false
|
||||||
|
|
||||||
const doc = object
|
const doc = object
|
||||||
|
|
||||||
@ -130,17 +131,21 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isFreeze (): boolean {
|
||||||
|
return freeze
|
||||||
|
}
|
||||||
|
|
||||||
$: displayMessages = filterChatMessages(messages, filters, filterResources, doc._class, selectedFilters)
|
$: displayMessages = filterChatMessages(messages, filters, filterResources, doc._class, selectedFilters)
|
||||||
|
|
||||||
const unsubscribe = inboxClient.inboxNotificationsByContext.subscribe(() => {
|
const unsubscribe = inboxClient.inboxNotificationsByContext.subscribe(() => {
|
||||||
if (notifyContext !== undefined) {
|
if (notifyContext !== undefined && !isFreeze()) {
|
||||||
recheckNotifications(notifyContext)
|
recheckNotifications(notifyContext)
|
||||||
readViewportMessages()
|
readViewportMessages()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function scrollToBottom (afterScrollFn?: () => void): void {
|
function scrollToBottom (afterScrollFn?: () => void): void {
|
||||||
if (scroller != null && scrollElement != null) {
|
if (scroller != null && scrollElement != null && !isFreeze()) {
|
||||||
scroller.scrollBy(scrollElement.scrollHeight)
|
scroller.scrollBy(scrollElement.scrollHeight)
|
||||||
updateSelectedDate()
|
updateSelectedDate()
|
||||||
afterScrollFn?.()
|
afterScrollFn?.()
|
||||||
@ -338,7 +343,7 @@
|
|||||||
let messagesToReadAccumulatorTimer: any
|
let messagesToReadAccumulatorTimer: any
|
||||||
|
|
||||||
function readViewportMessages (): void {
|
function readViewportMessages (): void {
|
||||||
if (!scrollElement || !scrollContentBox) {
|
if (!scrollElement || !scrollContentBox || isFreeze()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,7 +457,6 @@
|
|||||||
isInitialScrolling = false
|
isInitialScrolling = false
|
||||||
} else if (separatorIndex === -1) {
|
} else if (separatorIndex === -1) {
|
||||||
await wait()
|
await wait()
|
||||||
if (initialScrollBottom) {
|
|
||||||
isScrollInitialized = true
|
isScrollInitialized = true
|
||||||
shouldWaitAndRead = true
|
shouldWaitAndRead = true
|
||||||
autoscroll = true
|
autoscroll = true
|
||||||
@ -461,13 +465,6 @@
|
|||||||
waitLastMessageRenderAndRead(() => {
|
waitLastMessageRenderAndRead(() => {
|
||||||
autoscroll = false
|
autoscroll = false
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
isScrollInitialized = true
|
|
||||||
autoscroll = false
|
|
||||||
updateShouldScrollToNew()
|
|
||||||
isInitialScrolling = false
|
|
||||||
readViewportMessages()
|
|
||||||
}
|
|
||||||
} else if (separatorElement) {
|
} else if (separatorElement) {
|
||||||
await wait()
|
await wait()
|
||||||
scrollToSeparator()
|
scrollToSeparator()
|
||||||
@ -519,6 +516,7 @@
|
|||||||
|
|
||||||
function scrollToNewMessages (): void {
|
function scrollToNewMessages (): void {
|
||||||
if (!scrollElement || !shouldScrollToNew) {
|
if (!scrollElement || !shouldScrollToNew) {
|
||||||
|
readViewportMessages()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -557,14 +555,22 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFreeze()) {
|
||||||
|
messagesCount = newCount
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (scrollToRestore > 0) {
|
if (scrollToRestore > 0) {
|
||||||
void restoreScroll()
|
void restoreScroll()
|
||||||
} else if (dateToJump !== undefined) {
|
} else if (dateToJump !== undefined) {
|
||||||
await wait()
|
await wait()
|
||||||
scrollToDate(dateToJump)
|
scrollToDate(dateToJump)
|
||||||
} else if (messagesCount > 0 && newCount > messagesCount) {
|
} else if (shouldScrollToNew && messagesCount > 0 && newCount > messagesCount) {
|
||||||
await wait()
|
await wait()
|
||||||
scrollToNewMessages()
|
scrollToNewMessages()
|
||||||
|
} else {
|
||||||
|
await wait()
|
||||||
|
readViewportMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesCount = newCount
|
messagesCount = newCount
|
||||||
@ -576,7 +582,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldScrollToNew && initialScrollBottom) {
|
if (shouldScrollToNew) {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,7 +666,7 @@
|
|||||||
} else if (element != null) {
|
} else if (element != null) {
|
||||||
const { scrollHeight, scrollTop, offsetHeight } = element
|
const { scrollHeight, scrollTop, offsetHeight } = element
|
||||||
|
|
||||||
showScrollDownButton = scrollHeight > offsetHeight + scrollTop + 300
|
showScrollDownButton = scrollHeight > offsetHeight + scrollTop + 50
|
||||||
} else {
|
} else {
|
||||||
showScrollDownButton = false
|
showScrollDownButton = false
|
||||||
}
|
}
|
||||||
@ -691,7 +697,7 @@
|
|||||||
$: void forceReadContext(isScrollAtBottom, notifyContext)
|
$: void forceReadContext(isScrollAtBottom, notifyContext)
|
||||||
|
|
||||||
async function forceReadContext (isScrollAtBottom: boolean, context?: DocNotifyContext): Promise<void> {
|
async function forceReadContext (isScrollAtBottom: boolean, context?: DocNotifyContext): Promise<void> {
|
||||||
if (context === undefined || !isScrollAtBottom || forceRead) return
|
if (context === undefined || !isScrollAtBottom || forceRead || isFreeze()) return
|
||||||
const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context
|
const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context
|
||||||
|
|
||||||
if (lastViewedTimestamp >= lastUpdateTimestamp) return
|
if (lastViewedTimestamp >= lastUpdateTimestamp) return
|
||||||
@ -708,6 +714,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canLoadNextForwardStore = provider.canLoadNextForwardStore
|
const canLoadNextForwardStore = provider.canLoadNextForwardStore
|
||||||
|
|
||||||
|
$: if (!freeze) {
|
||||||
|
readViewportMessages()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
@ -777,6 +787,16 @@
|
|||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
{#if !fixedInput}
|
||||||
|
<div class="ref-input flex-col">
|
||||||
|
<ActivityExtensionComponent
|
||||||
|
kind="input"
|
||||||
|
{extensions}
|
||||||
|
props={{ object, boundary: scrollElement, collection, autofocus: true, withTypingInfo: true }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if loadMoreAllowed && $canLoadNextForwardStore}
|
{#if loadMoreAllowed && $canLoadNextForwardStore}
|
||||||
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
||||||
{/if}
|
{/if}
|
||||||
@ -794,7 +814,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if object}
|
{#if fixedInput && object}
|
||||||
<div class="ref-input flex-col">
|
<div class="ref-input flex-col">
|
||||||
<ActivityExtensionComponent
|
<ActivityExtensionComponent
|
||||||
kind="input"
|
kind="input"
|
||||||
@ -841,7 +861,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
bottom: -0.75rem;
|
bottom: -0.75rem;
|
||||||
animation: 1s fadeIn;
|
animation: 0.5s fadeIn;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,14 @@
|
|||||||
{ limit: 1 }
|
{ limit: 1 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
let renderChannel = tab.data.thread === undefined
|
||||||
|
|
||||||
|
$: if (tab.data.thread === undefined) {
|
||||||
|
renderChannel = true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if object}
|
{#if object && renderChannel}
|
||||||
<div class="channel" class:invisible={threadId !== undefined} style:height style:width>
|
<div class="channel" class:invisible={threadId !== undefined} style:height style:width>
|
||||||
<ChannelHeader
|
<ChannelHeader
|
||||||
_id={object._id}
|
_id={object._id}
|
||||||
@ -71,16 +76,17 @@
|
|||||||
canOpen={true}
|
canOpen={true}
|
||||||
allowClose={true}
|
allowClose={true}
|
||||||
canOpenInSidebar={false}
|
canOpenInSidebar={false}
|
||||||
|
closeOnEscape={false}
|
||||||
on:close
|
on:close
|
||||||
/>
|
/>
|
||||||
{#key object._id}
|
{#key object._id}
|
||||||
<Channel {object} {context} syncLocation={false} />
|
<Channel {object} {context} syncLocation={false} freeze={threadId !== undefined} />
|
||||||
{/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:close={() => closeThreadInSidebarChannel(widget, tab)} />
|
<ThreadView _id={threadId} on:channel={() => closeThreadInSidebarChannel(widget, tab)} on:close />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="popupPanel panel">
|
<div class="popupPanel">
|
||||||
<ChannelHeader
|
<ChannelHeader
|
||||||
_id={object._id}
|
_id={object._id}
|
||||||
_class={object._class}
|
_class={object._class}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if widget && tab && tab.type === 'channel'}
|
{#if widget && tab}
|
||||||
<ChannelSidebarView
|
<ChannelSidebarView
|
||||||
{widget}
|
{widget}
|
||||||
{tab}
|
{tab}
|
||||||
@ -46,13 +46,4 @@
|
|||||||
handleClose(tab?.id)
|
handleClose(tab?.id)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if widget && tab && tab.type === 'thread'}
|
|
||||||
<ThreadSidebarView
|
|
||||||
{tab}
|
|
||||||
{height}
|
|
||||||
{width}
|
|
||||||
on:close={() => {
|
|
||||||
handleClose(tab?.id)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
let count: number = 0
|
let count: number = 0
|
||||||
|
|
||||||
$: objectId = tab.type === 'thread' ? tab.data.thread : tab.data._id
|
$: objectId = tab.data.thread ?? tab.data._id
|
||||||
$: context = objectId ? $contextByDocStore.get(objectId) : undefined
|
$: context = objectId ? $contextByDocStore.get(objectId) : undefined
|
||||||
|
|
||||||
const unsubscribe = notificationClient.inboxNotificationsByContext.subscribe((res) => {
|
const unsubscribe = notificationClient.inboxNotificationsByContext.subscribe((res) => {
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
export let adaptive: HeaderAdaptive = 'default'
|
export let adaptive: HeaderAdaptive = 'default'
|
||||||
export let hideActions: boolean = false
|
export let hideActions: boolean = false
|
||||||
export let canOpenInSidebar: boolean = false
|
export let canOpenInSidebar: boolean = false
|
||||||
|
export let closeOnEscape: boolean = true
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
@ -72,6 +73,7 @@
|
|||||||
hideActions={!((canOpen && object) || withAside || $$slots.actions) || hideActions}
|
hideActions={!((canOpen && object) || withAside || $$slots.actions) || hideActions}
|
||||||
hideDescription={!description}
|
hideDescription={!description}
|
||||||
adaptive={adaptive !== 'default' ? adaptive : withFilters ? 'freezeActions' : 'disabled'}
|
adaptive={adaptive !== 'default' ? adaptive : withFilters ? 'freezeActions' : 'disabled'}
|
||||||
|
{closeOnEscape}
|
||||||
on:click
|
on:click
|
||||||
on:close
|
on:close
|
||||||
>
|
>
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
bind:this={div}
|
bind:this={div}
|
||||||
class="border-radius-4 over-underline dateSelectorButton clear-mins"
|
class="border-radius-4 dateSelectorButton clear-mins"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showPopup(DateRangePopup, {}, div, (v) => {
|
showPopup(DateRangePopup, {}, div, (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
@ -68,13 +68,17 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: var(--theme-divider-color);
|
background-color: var(--highlight-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dateSelectorButton {
|
.dateSelectorButton {
|
||||||
|
cursor: pointer;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
background-color: var(--theme-list-row-color);
|
color: var(--theme-content-color);
|
||||||
border: 1px solid var(--theme-divider-color);
|
background-color: var(--highlight-select);
|
||||||
|
border: 1px solid var(--highlight-select-border);
|
||||||
|
font-weight: 500;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,20 +14,15 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Label } from '@hcengineering/ui'
|
import { Label } from '@hcengineering/ui'
|
||||||
import notification from '@hcengineering/notification'
|
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
import { Timestamp } from '@hcengineering/core'
|
import { Timestamp } from '@hcengineering/core'
|
||||||
|
|
||||||
export let editedOn: Timestamp | undefined
|
|
||||||
export let label: IntlString | undefined
|
export let label: IntlString | undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if label}
|
{#if label}
|
||||||
<span class="text-sm lower"> <Label {label} /></span>
|
<span class="text-sm lower"> <Label {label} /></span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if editedOn}
|
|
||||||
<span class="text-sm lower"><Label label={notification.string.Edited} /></span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
span {
|
span {
|
||||||
|
@ -255,7 +255,7 @@
|
|||||||
{onClick}
|
{onClick}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="header">
|
<svelte:fragment slot="header">
|
||||||
<ChatMessageHeader editedOn={value.editedOn} label={skipLabel ? undefined : viewlet?.label} />
|
<ChatMessageHeader label={viewlet?.label} />
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
{#if !isEditing}
|
{#if !isEditing}
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
if (direct !== undefined && missingAccounts.length > 0) {
|
if (direct !== undefined && missingAccounts.length > 0) {
|
||||||
await client.updateDoc(chunter.class.DirectMessage, direct.space, direct._id, {
|
await client.updateDoc(chunter.class.DirectMessage, direct.space, direct._id, {
|
||||||
members: [...direct.members, ...missingAccounts]
|
$push: { members: { $each: missingAccounts, $position: 0 } }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
{#if tab.data.thread}
|
{#if tab.data.thread}
|
||||||
<div class="root" style:height style:width>
|
<div class="root" style:height style:width>
|
||||||
<ThreadView _id={tab.data.thread} on:close />
|
<ThreadView _id={tab.data.thread} on:close on:channel />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -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 } from '@hcengineering/ui'
|
import { Breadcrumbs, Label, location as locationStore, Header, BreadcrumbItem } 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'
|
||||||
@ -104,7 +104,14 @@
|
|||||||
channelName = res
|
channelName = res
|
||||||
})
|
})
|
||||||
|
|
||||||
function getBreadcrumbsItems (channel?: Doc, message?: DisplayActivityMessage, channelName?: string) {
|
let breadcrumbs: BreadcrumbItem[] = []
|
||||||
|
$: breadcrumbs = showHeader ? getBreadcrumbsItems(channel, message, channelName) : []
|
||||||
|
|
||||||
|
function getBreadcrumbsItems (
|
||||||
|
channel?: Doc,
|
||||||
|
message?: DisplayActivityMessage,
|
||||||
|
channelName?: string
|
||||||
|
): BreadcrumbItem[] {
|
||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -115,6 +122,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
id: 'channel',
|
||||||
icon: getObjectIcon(message.attachedToClass),
|
icon: getObjectIcon(message.attachedToClass),
|
||||||
iconProps: { value: channel },
|
iconProps: { value: channel },
|
||||||
iconWidth: isPersonAvatar ? 'auto' : undefined,
|
iconWidth: isPersonAvatar ? 'auto' : undefined,
|
||||||
@ -122,14 +130,24 @@
|
|||||||
title: channelName,
|
title: channelName,
|
||||||
label: channelName ? undefined : chunter.string.Channel
|
label: channelName ? undefined : chunter.string.Channel
|
||||||
},
|
},
|
||||||
{ label: chunter.string.Thread }
|
{ id: 'thread', label: chunter.string.Thread }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBreadcrumbSelect (event: CustomEvent<number>): void {
|
||||||
|
const index = event.detail
|
||||||
|
const breadcrumb = breadcrumbs[index]
|
||||||
|
|
||||||
|
if (breadcrumb === undefined) return
|
||||||
|
if (breadcrumb.id !== 'channel') return
|
||||||
|
|
||||||
|
dispatch('channel')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showHeader}
|
{#if showHeader}
|
||||||
<Header type={'type-aside'} adaptive={'disabled'} on:close>
|
<Header type={'type-aside'} adaptive={'disabled'} closeOnEscape={false} on:close>
|
||||||
<Breadcrumbs items={getBreadcrumbsItems(channel, message, channelName)} currentOnly />
|
<Breadcrumbs items={breadcrumbs} on:select={handleBreadcrumbSelect} selected={1} />
|
||||||
</Header>
|
</Header>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -141,8 +159,8 @@
|
|||||||
skipLabels
|
skipLabels
|
||||||
object={message}
|
object={message}
|
||||||
provider={dataProvider}
|
provider={dataProvider}
|
||||||
initialScrollBottom={false}
|
|
||||||
fullHeight={false}
|
fullHeight={false}
|
||||||
|
fixedInput={false}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="header">
|
<svelte:fragment slot="header">
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
|
@ -61,6 +61,7 @@ import {
|
|||||||
getThreadLink,
|
getThreadLink,
|
||||||
openChannelInSidebar,
|
openChannelInSidebar,
|
||||||
openChannelInSidebarAction,
|
openChannelInSidebarAction,
|
||||||
|
openThreadInSidebar,
|
||||||
replyToThread
|
replyToThread
|
||||||
} from './navigation'
|
} from './navigation'
|
||||||
import {
|
import {
|
||||||
@ -199,7 +200,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
GetMessageLink: getMessageLocation,
|
GetMessageLink: getMessageLocation,
|
||||||
CloseChatWidgetTab: closeChatWidgetTab,
|
CloseChatWidgetTab: closeChatWidgetTab,
|
||||||
OpenChannelInSidebar: openChannelInSidebar,
|
OpenChannelInSidebar: openChannelInSidebar,
|
||||||
CanTranslateMessage: canTranslateMessage
|
CanTranslateMessage: canTranslateMessage,
|
||||||
|
OpenThreadInSidebar: openThreadInSidebar
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
ArchiveChannel,
|
ArchiveChannel,
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
type Location,
|
type Location,
|
||||||
navigate
|
navigate
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { type Ref, type Doc, type Class } from '@hcengineering/core'
|
import { type Ref, type Doc, type Class, generateId } from '@hcengineering/core'
|
||||||
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
||||||
import {
|
import {
|
||||||
type Channel,
|
type Channel,
|
||||||
@ -262,11 +262,11 @@ export async function openChannelInSidebar (
|
|||||||
size: isDirect || isPerson ? 'tiny' : 'x-small',
|
size: isDirect || isPerson ? 'tiny' : 'x-small',
|
||||||
compact: true
|
compact: true
|
||||||
},
|
},
|
||||||
type: 'channel',
|
|
||||||
data: {
|
data: {
|
||||||
_id,
|
_id,
|
||||||
_class,
|
_class,
|
||||||
thread
|
thread,
|
||||||
|
channelName: name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,18 +288,26 @@ export async function openThreadInSidebarChannel (
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const newTab: ChatWidgetTab = {
|
const newTab: ChatWidgetTab = {
|
||||||
...tab,
|
...tab,
|
||||||
|
name: await translate(chunter.string.ThreadIn, { name: tab.data.channelName }),
|
||||||
data: { ...tab.data, thread: message._id }
|
data: { ...tab.data, thread: message._id }
|
||||||
}
|
}
|
||||||
createWidgetTab(widget, newTab)
|
createWidgetTab(widget, newTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidgetTab): Promise<void> {
|
export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidgetTab): Promise<void> {
|
||||||
|
const thread = tab.allowedPath !== undefined ? tab.data.thread : undefined
|
||||||
const newTab: ChatWidgetTab = {
|
const newTab: ChatWidgetTab = {
|
||||||
...tab,
|
...tab,
|
||||||
|
id: tab.id.startsWith('thread_') ? generateId() : tab.id,
|
||||||
|
name: tab.data.channelName,
|
||||||
|
allowedPath: undefined,
|
||||||
data: { ...tab.data, thread: undefined }
|
data: { ...tab.data, thread: undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
createWidgetTab(widget, newTab)
|
createWidgetTab(widget, newTab)
|
||||||
|
setTimeout(() => {
|
||||||
|
removeThreadFromLoc(thread)
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc): Promise<void> {
|
export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc): Promise<void> {
|
||||||
@ -321,9 +329,7 @@ export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: Acti
|
|||||||
const allowedPath = loc.path.join('/')
|
const allowedPath = loc.path.join('/')
|
||||||
|
|
||||||
const currentTAbs = get(sidebarStore).widgetsState.get(widget._id)?.tabs ?? []
|
const currentTAbs = get(sidebarStore).widgetsState.get(widget._id)?.tabs ?? []
|
||||||
const tabsToClose = currentTAbs
|
const tabsToClose = currentTAbs.filter((t) => t.isPinned !== true && t.allowedPath === allowedPath).map((t) => t.id)
|
||||||
.filter((t) => t.isPinned !== true && t.allowedPath === allowedPath && (t as ChatWidgetTab).type === 'thread')
|
|
||||||
.map((t) => t.id)
|
|
||||||
|
|
||||||
if (tabsToClose.length > 0) {
|
if (tabsToClose.length > 0) {
|
||||||
sidebarStore.update((s) => {
|
sidebarStore.update((s) => {
|
||||||
@ -341,26 +347,31 @@ export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: Acti
|
|||||||
name: tabName,
|
name: tabName,
|
||||||
icon: chunter.icon.Thread,
|
icon: chunter.icon.Thread,
|
||||||
allowedPath,
|
allowedPath,
|
||||||
type: 'thread',
|
|
||||||
data: {
|
data: {
|
||||||
_id: object?._id,
|
_id: object?._id,
|
||||||
_class: object?._class,
|
_class: object?._class,
|
||||||
thread: message._id
|
thread: message._id,
|
||||||
|
channelName: name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createWidgetTab(widget, tab, true)
|
createWidgetTab(widget, tab, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeChatWidgetTab (tab?: ChatWidgetTab): void {
|
export function removeThreadFromLoc (thread?: Ref<ActivityMessage>): void {
|
||||||
if (tab?.type === 'thread') {
|
if (thread === undefined) return
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
|
|
||||||
if (loc.path[2] === chunterId || loc.path[2] === notificationId) {
|
if (loc.path[2] === chunterId || loc.path[2] === notificationId) {
|
||||||
if (loc.path[4] === tab.data.thread) {
|
if (loc.path[4] === thread) {
|
||||||
loc.path[4] = ''
|
loc.path[4] = ''
|
||||||
loc.path.length = 4
|
loc.path.length = 4
|
||||||
navigate(loc)
|
navigate(loc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeChatWidgetTab (tab?: ChatWidgetTab): void {
|
||||||
|
if (tab?.allowedPath !== undefined) {
|
||||||
|
removeThreadFromLoc(tab.data.thread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,9 +434,13 @@ export async function readChannelMessages (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
if (context === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||||
const client = getClient().apply(undefined, 'readViewportMessages')
|
const client = getClient().apply(undefined, 'readViewportMessages')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const readMessages = get(chatReadMessagesStore)
|
const readMessages = get(chatReadMessagesStore)
|
||||||
const allIds = getAllIds(messages).filter((id) => !readMessages.has(id))
|
const allIds = getAllIds(messages).filter((id) => !readMessages.has(id))
|
||||||
@ -460,12 +464,6 @@ export async function readChannelMessages (
|
|||||||
|
|
||||||
chatReadMessagesStore.update((store) => new Set([...store, ...allIds]))
|
chatReadMessagesStore.update((store) => new Set([...store, ...allIds]))
|
||||||
|
|
||||||
await inboxClient.readNotifications(client, [...notifications, ...relatedMentions])
|
|
||||||
|
|
||||||
if (context === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const storedTimestampUpdates = get(contextsTimestampStore).get(context._id)
|
const storedTimestampUpdates = get(contextsTimestampStore).get(context._id)
|
||||||
const newTimestamp = messages[messages.length - 1].createdOn ?? 0
|
const newTimestamp = messages[messages.length - 1].createdOn ?? 0
|
||||||
const prevTimestamp = Math.max(storedTimestampUpdates ?? 0, context.lastViewedTimestamp ?? 0)
|
const prevTimestamp = Math.max(storedTimestampUpdates ?? 0, context.lastViewedTimestamp ?? 0)
|
||||||
@ -478,6 +476,7 @@ export async function readChannelMessages (
|
|||||||
})
|
})
|
||||||
await client.update(context, { lastViewedTimestamp: newTimestamp })
|
await client.update(context, { lastViewedTimestamp: newTimestamp })
|
||||||
}
|
}
|
||||||
|
await inboxClient.readNotifications(client, [...notifications, ...relatedMentions])
|
||||||
} finally {
|
} finally {
|
||||||
await client.commit()
|
await client.commit()
|
||||||
}
|
}
|
||||||
|
@ -100,8 +100,7 @@ export interface InlineButton extends AttachedDoc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatWidgetTab extends WidgetTab {
|
export interface ChatWidgetTab extends WidgetTab {
|
||||||
type: 'channel' | 'thread'
|
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>, channelName: string }
|
||||||
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage> }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -232,6 +231,7 @@ export default plugin(chunterId, {
|
|||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
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>>,
|
||||||
OpenChannelInSidebar: '' as Resource<
|
OpenChannelInSidebar: '' as Resource<
|
||||||
(_id: Ref<Doc>, _class: Ref<Doc>, doc?: Doc, thread?: Ref<ActivityMessage>) => Promise<void>
|
(_id: Ref<Doc>, _class: Ref<Doc>, doc?: Doc, thread?: Ref<ActivityMessage>) => Promise<void>
|
||||||
>
|
>
|
||||||
|
@ -32,13 +32,13 @@
|
|||||||
{#if value !== undefined}
|
{#if value !== undefined}
|
||||||
<div class:highlighted class="root">
|
<div class:highlighted class="root">
|
||||||
<div class="header pt-2 pb-2 pl-4 pr-4 flex-between">
|
<div class="header pt-2 pb-2 pl-4 pr-4 flex-between">
|
||||||
<div>
|
<span class="overflow-label">
|
||||||
{#if value?.index}
|
{#if value?.index}
|
||||||
<span>#{value.index}</span>
|
<span data-id="commentId">#{value.index}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
{/if}
|
{/if}
|
||||||
<Label label={resolved ? documents.string.Resolved : documents.string.Pending} />
|
<Label label={resolved ? documents.string.Resolved : documents.string.Pending} />
|
||||||
</div>
|
</span>
|
||||||
{#if $canAddDocumentCommentsFeedback}
|
{#if $canAddDocumentCommentsFeedback}
|
||||||
<div class="tools">
|
<div class="tools">
|
||||||
<Button
|
<Button
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const query = createQuery()
|
const query = createQuery()
|
||||||
|
const isSecureContext = window.isSecureContext
|
||||||
|
|
||||||
interface InviteParams {
|
interface InviteParams {
|
||||||
expirationTime: number
|
expirationTime: number
|
||||||
@ -87,6 +88,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function copy (): void {
|
function copy (): void {
|
||||||
|
if (!isSecureContext) return
|
||||||
if (link === undefined) return
|
if (link === undefined) return
|
||||||
copyTextToClipboard(link)
|
copyTextToClipboard(link)
|
||||||
copied = true
|
copied = true
|
||||||
@ -109,7 +111,7 @@
|
|||||||
let loading = false
|
let loading = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="antiPopup popup">
|
<div class="antiPopup popup" class:secure={isSecureContext}>
|
||||||
<div class="flex-between fs-title mb-9">
|
<div class="flex-between fs-title mb-9">
|
||||||
<Label label={login.string.InviteDescription} />
|
<Label label={login.string.InviteDescription} />
|
||||||
<InviteWorkspace size={'large'} />
|
<InviteWorkspace size={'large'} />
|
||||||
@ -154,7 +156,9 @@
|
|||||||
<Loading />
|
<Loading />
|
||||||
{:else if link !== undefined}
|
{:else if link !== undefined}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="over-underline link" on:click={copy}>{link}</div>
|
<div class="link" class:notSecure={!isSecureContext} class:over-underline={isSecureContext} on:click={copy}>
|
||||||
|
{link}
|
||||||
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
<Button
|
||||||
label={login.string.Close}
|
label={login.string.Close}
|
||||||
@ -164,7 +168,9 @@
|
|||||||
dispatch('close')
|
dispatch('close')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{#if isSecureContext}
|
||||||
<Button label={copied ? login.string.Copied : login.string.Copy} size={'medium'} on:click={copy} />
|
<Button label={copied ? login.string.Copied : login.string.Copy} size={'medium'} on:click={copy} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
@ -195,6 +201,10 @@
|
|||||||
.link {
|
.link {
|
||||||
margin: 1.75rem 0 0;
|
margin: 1.75rem 0 0;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
&.notSecure {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import { getProviders } from '../utils'
|
import { getProviders } from '../utils'
|
||||||
import Github from './providers/Github.svelte'
|
import Github from './providers/Github.svelte'
|
||||||
import Google from './providers/Google.svelte'
|
import Google from './providers/Google.svelte'
|
||||||
|
import OpenId from './providers/OpenId.svelte'
|
||||||
|
|
||||||
interface Provider {
|
interface Provider {
|
||||||
name: string
|
name: string
|
||||||
@ -21,6 +22,10 @@
|
|||||||
{
|
{
|
||||||
name: 'github',
|
name: 'github',
|
||||||
component: Github
|
component: Github
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'openid',
|
||||||
|
component: OpenId
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
15
plugins/login-resources/src/components/icons/OpenId.svelte
Normal file
15
plugins/login-resources/src/components/icons/OpenId.svelte
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg viewBox="0 0 512 512" width="1.5rem" height="1.5rem" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect
|
||||||
|
height="512"
|
||||||
|
rx="64"
|
||||||
|
ry="64"
|
||||||
|
style="fill:#f68423;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
width="512"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 416.99957,216.95084 c -39.2625,-22.18682 -78.9827,-34.64025 -117.0052,-39.52028 V 73.776502 l -60.66712,39.049158 v 63.02711 c -188.010218,14.68899 -295.068752,208.65924 0,262.37073 l 60.66712,-39.04916 V 218.28862 c 24.3468,4.22226 50.4759,12.17342 78.0094,24.69351 l -34.6758,21.70238 h 112.6719 v -73.76497 l -39.0003,26.0313 z m -177.67682,-1.66668 v 183.89018 c -182.841238,-23.72461 -140.809838,-171.94788 0,-183.89018 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 751 B |
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Label } from '@hcengineering/ui'
|
||||||
|
import OpenId from '../icons/OpenId.svelte'
|
||||||
|
import login from '../../plugin'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex-row-center flex-gap-2">
|
||||||
|
<OpenId />
|
||||||
|
<Label label={login.string.ContinueWith} params={{ provider: 'OpenId' }} />
|
||||||
|
</div>
|
@ -37,6 +37,7 @@
|
|||||||
import view, { decodeObjectURI } from '@hcengineering/view'
|
import view, { decodeObjectURI } from '@hcengineering/view'
|
||||||
import { parseLinkId } from '@hcengineering/view-resources'
|
import { parseLinkId } from '@hcengineering/view-resources'
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
|
import { getResource } from '@hcengineering/platform'
|
||||||
|
|
||||||
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
||||||
import notification from '../../plugin'
|
import notification from '../../plugin'
|
||||||
@ -179,6 +180,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedMessageId = loc?.loc.query?.message as Ref<ActivityMessage> | undefined
|
const selectedMessageId = loc?.loc.query?.message as Ref<ActivityMessage> | undefined
|
||||||
|
const thread = loc?.loc.path[4] as Ref<ActivityMessage> | undefined
|
||||||
|
|
||||||
|
if (thread !== undefined) {
|
||||||
|
const fn = await getResource(chunter.function.OpenThreadInSidebar)
|
||||||
|
void fn(thread)
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedMessageId !== undefined) {
|
if (selectedMessageId !== undefined) {
|
||||||
selectedMessage = get(inboxClient.activityInboxNotifications).find(
|
selectedMessage = get(inboxClient.activityInboxNotifications).find(
|
||||||
|
@ -588,6 +588,7 @@ export async function selectInboxContext (
|
|||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
const { objectId, objectClass } = context
|
const { objectId, objectClass } = context
|
||||||
|
const loc = getCurrentLocation()
|
||||||
|
|
||||||
if (isMentionNotification(notification) && isActivityMessageClass(notification.mentionedInClass)) {
|
if (isMentionNotification(notification) && isActivityMessageClass(notification.mentionedInClass)) {
|
||||||
const selectedMsg = notification.mentionedIn as Ref<ActivityMessage>
|
const selectedMsg = notification.mentionedIn as Ref<ActivityMessage>
|
||||||
@ -619,17 +620,25 @@ export async function selectInboxContext (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isReactionMessage(message)) {
|
if (isReactionMessage(message)) {
|
||||||
void navigateToInboxDoc(linkProviders, objectId, objectClass, undefined, objectId as Ref<ActivityMessage>)
|
const thread = loc.path[4] === objectId ? objectId : undefined
|
||||||
|
void navigateToInboxDoc(
|
||||||
|
linkProviders,
|
||||||
|
objectId,
|
||||||
|
objectClass,
|
||||||
|
thread as Ref<ActivityMessage>,
|
||||||
|
objectId as Ref<ActivityMessage>
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedMsg = (notification as ActivityInboxNotification)?.attachedTo
|
const selectedMsg = (notification as ActivityInboxNotification)?.attachedTo
|
||||||
|
const thread = selectedMsg !== objectId ? objectId : loc.path[4] === objectId ? objectId : undefined
|
||||||
|
|
||||||
void navigateToInboxDoc(
|
void navigateToInboxDoc(
|
||||||
linkProviders,
|
linkProviders,
|
||||||
objectId,
|
objectId,
|
||||||
objectClass,
|
objectClass,
|
||||||
selectedMsg !== undefined ? (objectId as Ref<ActivityMessage>) : undefined,
|
thread as Ref<ActivityMessage>,
|
||||||
selectedMsg ?? (objectId as Ref<ActivityMessage>)
|
selectedMsg ?? (objectId as Ref<ActivityMessage>)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AccordionItem {id} {icon} {iconProps} {title} {label} size={'medium'} nested>
|
<AccordionItem {id} {icon} {iconProps} {title} {label} size={'medium'} nested hiddenHeader={project === false}>
|
||||||
{#each todos as todo, index}
|
{#each todos as todo, index}
|
||||||
<ToDoDraggable {todo} {index} {groupName} {projectId} on:drop={handleDrop}>
|
<ToDoDraggable {todo} {index} {groupName} {projectId} on:drop={handleDrop}>
|
||||||
<ToDoElement {todo} planned={mode !== 'unplanned'} />
|
<ToDoElement {todo} planned={mode !== 'unplanned'} />
|
||||||
|
@ -237,9 +237,9 @@
|
|||||||
active: WithLookup<ToDo>[]
|
active: WithLookup<ToDo>[]
|
||||||
): [IntlString, WithLookup<ToDo>[]][] {
|
): [IntlString, WithLookup<ToDo>[]][] {
|
||||||
const groups = new Map<IntlString, WithLookup<ToDo>[]>([
|
const groups = new Map<IntlString, WithLookup<ToDo>[]>([
|
||||||
|
[time.string.Scheduled, []],
|
||||||
[time.string.Unplanned, unplanned],
|
[time.string.Unplanned, unplanned],
|
||||||
[time.string.ToDos, []],
|
[time.string.ToDos, []],
|
||||||
[time.string.Scheduled, []],
|
|
||||||
[time.string.Done, done]
|
[time.string.Done, done]
|
||||||
])
|
])
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
@ -135,7 +135,8 @@ import {
|
|||||||
canDeleteSpace,
|
canDeleteSpace,
|
||||||
canEditSpace,
|
canEditSpace,
|
||||||
canJoinSpace,
|
canJoinSpace,
|
||||||
canLeaveSpace
|
canLeaveSpace,
|
||||||
|
isClipboardAvailable
|
||||||
} from './visibilityTester'
|
} from './visibilityTester'
|
||||||
export { canArchiveSpace, canDeleteObject, canDeleteSpace, canEditSpace } from './visibilityTester'
|
export { canArchiveSpace, canDeleteObject, canDeleteSpace, canEditSpace } from './visibilityTester'
|
||||||
export { getActions, getContextActions, invokeAction, showMenu } from './actions'
|
export { getActions, getContextActions, invokeAction, showMenu } from './actions'
|
||||||
@ -334,6 +335,7 @@ export default async (): Promise<Resources> => ({
|
|||||||
CanDeleteSpace: canDeleteSpace,
|
CanDeleteSpace: canDeleteSpace,
|
||||||
CanJoinSpace: canJoinSpace,
|
CanJoinSpace: canJoinSpace,
|
||||||
CanLeaveSpace: canLeaveSpace,
|
CanLeaveSpace: canLeaveSpace,
|
||||||
|
IsClipboardAvailable: isClipboardAvailable,
|
||||||
BlobImageMetadata: blobImageMetadata,
|
BlobImageMetadata: blobImageMetadata,
|
||||||
BlobVideoMetadata: blobVideoMetadata
|
BlobVideoMetadata: blobVideoMetadata
|
||||||
}
|
}
|
||||||
|
@ -143,3 +143,7 @@ export async function canLeaveSpace (doc?: Doc | Doc[]): Promise<boolean> {
|
|||||||
|
|
||||||
return space.members?.includes(getCurrentAccount()._id)
|
return space.members?.includes(getCurrentAccount()._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isClipboardAvailable (doc?: Doc | Doc[]): boolean {
|
||||||
|
return isSecureContext && navigator.clipboard !== undefined
|
||||||
|
}
|
||||||
|
@ -100,6 +100,7 @@
|
|||||||
hideActions={false}
|
hideActions={false}
|
||||||
hideDescription={true}
|
hideDescription={true}
|
||||||
adaptive="disabled"
|
adaptive="disabled"
|
||||||
|
closeOnEscape={false}
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
if (widget !== undefined) {
|
if (widget !== undefined) {
|
||||||
closeWidget(widget._id)
|
closeWidget(widget._id)
|
||||||
@ -168,5 +169,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -101,7 +101,12 @@ export function openWidget (widget: Widget, data?: Record<string, any>, active =
|
|||||||
const { widgetsState } = state
|
const { widgetsState } = state
|
||||||
const widgetState = widgetsState.get(widget._id)
|
const widgetState = widgetsState.get(widget._id)
|
||||||
|
|
||||||
widgetsState.set(widget._id, { _id: widget._id, data, tab: widgetState?.tab, tabs: widgetState?.tabs ?? [] })
|
widgetsState.set(widget._id, {
|
||||||
|
_id: widget._id,
|
||||||
|
data: data ?? widgetState?.data,
|
||||||
|
tab: widgetState?.tab,
|
||||||
|
tabs: widgetState?.tabs ?? []
|
||||||
|
})
|
||||||
|
|
||||||
sidebarStore.set({
|
sidebarStore.set({
|
||||||
...state,
|
...state,
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
"passport-custom": "~1.1.1",
|
"passport-custom": "~1.1.1",
|
||||||
"passport-google-oauth20": "~2.0.0",
|
"passport-google-oauth20": "~2.0.0",
|
||||||
"passport-github2": "~0.1.12",
|
"passport-github2": "~0.1.12",
|
||||||
|
"openid-client": "~5.7.0",
|
||||||
"koa-passport": "^6.0.0",
|
"koa-passport": "^6.0.0",
|
||||||
"koa": "^2.15.3",
|
"koa": "^2.15.3",
|
||||||
"koa-router": "^12.0.1",
|
"koa-router": "^12.0.1",
|
||||||
|
@ -12,7 +12,7 @@ export function registerGithub (
|
|||||||
passport: Passport,
|
passport: Passport,
|
||||||
router: Router<any, any>,
|
router: Router<any, any>,
|
||||||
accountsUrl: string,
|
accountsUrl: string,
|
||||||
db: Db,
|
dbPromise: Promise<Db>,
|
||||||
frontUrl: string,
|
frontUrl: string,
|
||||||
brandings: BrandingMap
|
brandings: BrandingMap
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
@ -69,6 +69,7 @@ export function registerGithub (
|
|||||||
let loginInfo: LoginInfo
|
let loginInfo: LoginInfo
|
||||||
const state = safeParseAuthState(ctx.query?.state)
|
const state = safeParseAuthState(ctx.query?.state)
|
||||||
const branding = getBranding(brandings, state?.branding)
|
const branding = getBranding(brandings, state?.branding)
|
||||||
|
const db = await dbPromise
|
||||||
if (state.inviteId != null && state.inviteId !== '') {
|
if (state.inviteId != null && state.inviteId !== '') {
|
||||||
loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any, {
|
loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any, {
|
||||||
githubId: ctx.state.user.id
|
githubId: ctx.state.user.id
|
||||||
|
@ -12,7 +12,7 @@ export function registerGoogle (
|
|||||||
passport: Passport,
|
passport: Passport,
|
||||||
router: Router<any, any>,
|
router: Router<any, any>,
|
||||||
accountsUrl: string,
|
accountsUrl: string,
|
||||||
db: Db,
|
dbPromise: Promise<Db>,
|
||||||
frontUrl: string,
|
frontUrl: string,
|
||||||
brandings: BrandingMap
|
brandings: BrandingMap
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
@ -74,6 +74,7 @@ export function registerGoogle (
|
|||||||
let loginInfo: LoginInfo
|
let loginInfo: LoginInfo
|
||||||
const state = safeParseAuthState(ctx.query?.state)
|
const state = safeParseAuthState(ctx.query?.state)
|
||||||
const branding = getBranding(brandings, state?.branding)
|
const branding = getBranding(brandings, state?.branding)
|
||||||
|
const db = await dbPromise
|
||||||
if (state.inviteId != null && state.inviteId !== '') {
|
if (state.inviteId != null && state.inviteId !== '') {
|
||||||
loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any)
|
loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any)
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,6 +5,7 @@ import session from 'koa-session'
|
|||||||
import { Db } from 'mongodb'
|
import { Db } from 'mongodb'
|
||||||
import { registerGithub } from './github'
|
import { registerGithub } from './github'
|
||||||
import { registerGoogle } from './google'
|
import { registerGoogle } from './google'
|
||||||
|
import { registerOpenid } from './openid'
|
||||||
import { registerToken } from './token'
|
import { registerToken } from './token'
|
||||||
import { BrandingMap, MeasureContext } from '@hcengineering/core'
|
import { BrandingMap, MeasureContext } from '@hcengineering/core'
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ export type AuthProvider = (
|
|||||||
passport: Passport,
|
passport: Passport,
|
||||||
router: Router<any, any>,
|
router: Router<any, any>,
|
||||||
accountsUrl: string,
|
accountsUrl: string,
|
||||||
db: Db,
|
db: Promise<Db>,
|
||||||
frontUrl: string,
|
frontUrl: string,
|
||||||
brandings: BrandingMap
|
brandings: BrandingMap
|
||||||
) => string | undefined
|
) => string | undefined
|
||||||
@ -24,7 +25,7 @@ export function registerProviders (
|
|||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
app: Koa<Koa.DefaultState, Koa.DefaultContext>,
|
app: Koa<Koa.DefaultState, Koa.DefaultContext>,
|
||||||
router: Router<any, any>,
|
router: Router<any, any>,
|
||||||
db: Db,
|
db: Promise<Db>,
|
||||||
serverSecret: string,
|
serverSecret: string,
|
||||||
frontUrl: string | undefined,
|
frontUrl: string | undefined,
|
||||||
brandings: BrandingMap
|
brandings: BrandingMap
|
||||||
@ -60,7 +61,7 @@ export function registerProviders (
|
|||||||
registerToken(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
|
registerToken(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
|
||||||
|
|
||||||
const res: string[] = []
|
const res: string[] = []
|
||||||
const providers: AuthProvider[] = [registerGoogle, registerGithub]
|
const providers: AuthProvider[] = [registerGoogle, registerGithub, registerOpenid]
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
const value = provider(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
|
const value = provider(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
|
||||||
if (value !== undefined) res.push(value)
|
if (value !== undefined) res.push(value)
|
||||||
|
119
pods/authProviders/src/openid.ts
Normal file
119
pods/authProviders/src/openid.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
//
|
||||||
|
// 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 f.
|
||||||
|
//
|
||||||
|
import { joinWithProvider, loginWithProvider, type LoginInfo } from '@hcengineering/account'
|
||||||
|
import { BrandingMap, concatLink, MeasureContext, getBranding } from '@hcengineering/core'
|
||||||
|
import Router from 'koa-router'
|
||||||
|
import { Db } from 'mongodb'
|
||||||
|
import { Issuer, Strategy } from 'openid-client'
|
||||||
|
import qs from 'querystringify'
|
||||||
|
|
||||||
|
import { Passport } from '.'
|
||||||
|
import { getHost, safeParseAuthState } from './utils'
|
||||||
|
|
||||||
|
export function registerOpenid (
|
||||||
|
measureCtx: MeasureContext,
|
||||||
|
passport: Passport,
|
||||||
|
router: Router<any, any>,
|
||||||
|
accountsUrl: string,
|
||||||
|
dbPromise: Promise<Db>,
|
||||||
|
frontUrl: string,
|
||||||
|
brandings: BrandingMap
|
||||||
|
): string | undefined {
|
||||||
|
const openidClientId = process.env.OPENID_CLIENT_ID
|
||||||
|
const openidClientSecret = process.env.OPENID_CLIENT_SECRET
|
||||||
|
const issuer = process.env.OPENID_ISSUER
|
||||||
|
|
||||||
|
const redirectURL = '/auth/openid/callback'
|
||||||
|
if (openidClientId === undefined || openidClientSecret === undefined || issuer === undefined) return
|
||||||
|
|
||||||
|
void Issuer.discover(issuer).then((issuerObj) => {
|
||||||
|
const client = new issuerObj.Client({
|
||||||
|
client_id: openidClientId,
|
||||||
|
client_secret: openidClientSecret,
|
||||||
|
redirect_uris: [concatLink(accountsUrl, redirectURL)],
|
||||||
|
response_types: ['code']
|
||||||
|
})
|
||||||
|
|
||||||
|
passport.use(
|
||||||
|
'oidc',
|
||||||
|
new Strategy({ client, passReqToCallback: true }, (req: any, tokenSet: any, userinfo: any, done: any) => {
|
||||||
|
return done(null, userinfo)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/auth/openid', async (ctx, next) => {
|
||||||
|
measureCtx.info('try auth via', { provider: 'openid' })
|
||||||
|
const host = getHost(ctx.request.headers)
|
||||||
|
const brandingKey = host !== undefined ? brandings[host]?.key ?? undefined : undefined
|
||||||
|
const state = encodeURIComponent(
|
||||||
|
JSON.stringify({
|
||||||
|
inviteId: ctx.query?.inviteId,
|
||||||
|
branding: brandingKey
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await passport.authenticate('oidc', {
|
||||||
|
scope: 'openid profile email',
|
||||||
|
state
|
||||||
|
})(ctx, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
redirectURL,
|
||||||
|
async (ctx, next) => {
|
||||||
|
const state = safeParseAuthState(ctx.query?.state)
|
||||||
|
const branding = getBranding(brandings, state?.branding)
|
||||||
|
|
||||||
|
await passport.authenticate('oidc', {
|
||||||
|
failureRedirect: concatLink(branding?.front ?? frontUrl, '/login')
|
||||||
|
})(ctx, next)
|
||||||
|
},
|
||||||
|
async (ctx, next) => {
|
||||||
|
try {
|
||||||
|
const email = ctx.state.user.email ?? `openid:${ctx.state.user.sub}`
|
||||||
|
const [first, last] = ctx.state.user.name?.split(' ') ?? [ctx.state.user.username, '']
|
||||||
|
measureCtx.info('Provider auth handler', { email, type: 'openid' })
|
||||||
|
if (email !== undefined) {
|
||||||
|
let loginInfo: LoginInfo
|
||||||
|
const state = safeParseAuthState(ctx.query?.state)
|
||||||
|
const branding = getBranding(brandings, state?.branding)
|
||||||
|
const db = await dbPromise
|
||||||
|
if (state.inviteId != null && state.inviteId !== '') {
|
||||||
|
loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any, {
|
||||||
|
openId: ctx.state.user.sub
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
loginInfo = await loginWithProvider(measureCtx, db, null, email, first, last, {
|
||||||
|
openId: ctx.state.user.sub
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
|
||||||
|
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
|
||||||
|
|
||||||
|
measureCtx.info('Success auth, redirect', { email, type: 'openid', target: origin })
|
||||||
|
// Successful authentication, redirect to your application
|
||||||
|
ctx.redirect(`${origin}?${query}`)
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
measureCtx.error('failed to auth', { err, type: 'openid', user: ctx.state?.user })
|
||||||
|
}
|
||||||
|
await next()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return 'openid'
|
||||||
|
}
|
@ -12,7 +12,7 @@ export function registerToken (
|
|||||||
passport: Passport,
|
passport: Passport,
|
||||||
router: Router<any, any>,
|
router: Router<any, any>,
|
||||||
accountsUrl: string,
|
accountsUrl: string,
|
||||||
db: Db,
|
dbPromise: Promise<Db>,
|
||||||
frontUrl: string,
|
frontUrl: string,
|
||||||
brandings: BrandingMap
|
brandings: BrandingMap
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
@ -21,10 +21,12 @@ export function registerToken (
|
|||||||
new CustomStrategy(function (req: any, done: any) {
|
new CustomStrategy(function (req: any, done: any) {
|
||||||
const token = req.body.token ?? req.query.token
|
const token = req.body.token ?? req.query.token
|
||||||
|
|
||||||
|
void dbPromise.then((db) => {
|
||||||
getAccountInfoByToken(measureCtx, db, null, token)
|
getAccountInfoByToken(measureCtx, db, null, token)
|
||||||
.then((user: any) => done(null, user))
|
.then((user: any) => done(null, user))
|
||||||
.catch((err: any) => done(err))
|
.catch((err: any) => done(err))
|
||||||
})
|
})
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
@ -76,15 +76,15 @@ export class DocumentCommentsPage extends DocumentCommonPage {
|
|||||||
reply: string
|
reply: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const comment = this.page
|
const comment = this.page
|
||||||
.locator('div.popup div.root div.header > div > span:first-child', { hasText: String(commentId) })
|
.locator('div.text-editor-popup span[data-id="commentId"]', { hasText: `#${String(commentId)}` })
|
||||||
.locator('xpath=../../../..')
|
.locator('xpath=../../../..')
|
||||||
// check header
|
// check header
|
||||||
await expect(comment.locator('div.root > div.header > div:first-child')).toContainText(header)
|
await expect(comment.locator('div.root > div.header > span.overflow-label')).toContainText(header)
|
||||||
// can be resolved
|
// can be resolved
|
||||||
await comment.locator('div.header div.tools button').hover()
|
await comment.locator('div.header div.tools button').hover()
|
||||||
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
|
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
|
||||||
// check author
|
// check author
|
||||||
await expect(comment.locator('div.root div.header > div.username span.ap-label').first()).toHaveText(author)
|
await expect(comment.locator('div.activityMessage div.header a span[class*="label"]').first()).toHaveText(author)
|
||||||
// check message
|
// check message
|
||||||
await expect(
|
await expect(
|
||||||
comment.locator('div.activityMessage div.flex-col div.clear-mins div.text-markup-view > p').first()
|
comment.locator('div.activityMessage div.flex-col div.clear-mins div.text-markup-view > p').first()
|
||||||
@ -103,15 +103,15 @@ export class DocumentCommentsPage extends DocumentCommonPage {
|
|||||||
reply: string
|
reply: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const comment = this.page
|
const comment = this.page
|
||||||
.locator('div.box div.root div.header > div > span:first-child', { hasText: String(commentId) })
|
.locator('div[data-testid="comment"] span[data-id="commentId"]', { hasText: `#${String(commentId)}` })
|
||||||
.locator('xpath=../../../..')
|
.locator('xpath=../../../..')
|
||||||
// check header
|
// check header
|
||||||
await expect(comment.locator('div.root > div.header > div:first-child')).toContainText(header)
|
await expect(comment.locator('div.root > div.header > span.overflow-label')).toContainText(header)
|
||||||
// can be resolved
|
// can be resolved
|
||||||
await comment.locator('div.header > div > span:last-child').hover()
|
await comment.locator('div.root > div.header > span.overflow-label').first().hover()
|
||||||
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
|
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
|
||||||
// check author
|
// check author
|
||||||
await expect(comment.locator('div.root div.header > div.username span.ap-label').first()).toHaveText(author)
|
await expect(comment.locator('div.activityMessage div.header a span[class*="label"]').first()).toHaveText(author)
|
||||||
// check message
|
// check message
|
||||||
await expect(
|
await expect(
|
||||||
comment.locator('div.activityMessage div.flex-col div.clear-mins div.text-markup-view > p').first()
|
comment.locator('div.activityMessage div.flex-col div.clear-mins div.text-markup-view > p').first()
|
||||||
|
@ -103,10 +103,11 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap
|
|||||||
)
|
)
|
||||||
app.use(bodyParser())
|
app.use(bodyParser())
|
||||||
|
|
||||||
void client.getClient().then(async (p: MongoClient) => {
|
const mongoClientPromise = client.getClient()
|
||||||
const db = p.db(ACCOUNT_DB)
|
const dbPromise = mongoClientPromise.then((c) => c.db(ACCOUNT_DB))
|
||||||
registerProviders(measureCtx, app, router, db, serverSecret, frontURL, brandings)
|
registerProviders(measureCtx, app, router, dbPromise, serverSecret, frontURL, brandings)
|
||||||
|
|
||||||
|
void dbPromise.then((db) => {
|
||||||
setInterval(
|
setInterval(
|
||||||
() => {
|
() => {
|
||||||
void cleanExpiredOtp(db)
|
void cleanExpiredOtp(db)
|
||||||
|
@ -36,7 +36,7 @@ test.describe('Content in the Documents tests', () => {
|
|||||||
let documentsSecondPage: DocumentsPage
|
let documentsSecondPage: DocumentsPage
|
||||||
let documentContentSecondPage: DocumentContentPage
|
let documentContentSecondPage: DocumentContentPage
|
||||||
|
|
||||||
test.beforeEach(async ({ browser, page, request }) => {
|
test.beforeEach(async ({ page, request }) => {
|
||||||
leftSideMenuPage = new LeftSideMenuPage(page)
|
leftSideMenuPage = new LeftSideMenuPage(page)
|
||||||
documentsPage = new DocumentsPage(page)
|
documentsPage = new DocumentsPage(page)
|
||||||
documentContentPage = new DocumentContentPage(page)
|
documentContentPage = new DocumentContentPage(page)
|
||||||
@ -116,8 +116,8 @@ test.describe('Content in the Documents tests', () => {
|
|||||||
|
|
||||||
await documentContentPage.inputContentParapraph().click()
|
await documentContentPage.inputContentParapraph().click()
|
||||||
await documentContentPage.leftMenu().click()
|
await documentContentPage.leftMenu().click()
|
||||||
await documentContentPage.menuPopupItemButton('Table').click()
|
await documentContentPage.clickPopupItem('Table')
|
||||||
await documentContentPage.menuPopupItemButton('1x2').first().click()
|
await documentContentPage.clickPopupItem('1x2')
|
||||||
await documentContentPage.proseTableCell(0, 0).fill('One')
|
await documentContentPage.proseTableCell(0, 0).fill('One')
|
||||||
await documentContentPage.proseTableCell(0, 1).fill('Two')
|
await documentContentPage.proseTableCell(0, 1).fill('Two')
|
||||||
await documentContentPage.buttonInsertColumn().click()
|
await documentContentPage.buttonInsertColumn().click()
|
||||||
@ -295,4 +295,64 @@ test.describe('Content in the Documents tests', () => {
|
|||||||
await documentContentPage.page.keyboard.press('Escape')
|
await documentContentPage.page.keyboard.press('Escape')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Checking styles in a Document', async ({ page, browser, request }) => {
|
||||||
|
const content: string = [...new Array(20).keys()].map((index) => `Line ${index + 1}`).join('\n')
|
||||||
|
const testLink: string = 'http://test/link/123456'
|
||||||
|
const testNote: string = 'Test Note'
|
||||||
|
|
||||||
|
await documentContentPage.addContentToTheNewLine(content, false)
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 1', 'btnH1')
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 2', 'btnH2')
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 3', 'btnH3')
|
||||||
|
await documentContentPage.goToByTOC('Line 3')
|
||||||
|
await documentContentPage.goToByTOC('Line 1')
|
||||||
|
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 4', 'btnBold')
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 5', 'btnItalic')
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 6', 'btnStrikethrough')
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 7', 'btnUnderlined')
|
||||||
|
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 8', 'btnLink')
|
||||||
|
await documentContentPage.inputFormLink().fill(testLink)
|
||||||
|
await documentContentPage.buttonFormLinkSave().click()
|
||||||
|
|
||||||
|
await documentContentPage.addSeparator('Line 9')
|
||||||
|
for (let i = 9; i <= 11; i++) {
|
||||||
|
await documentContentPage.applyToolbarCommand(`Line ${i}`, 'btnOrderedList')
|
||||||
|
}
|
||||||
|
await documentContentPage.addSeparator('Line 12')
|
||||||
|
for (let i = 12; i <= 14; i++) {
|
||||||
|
await documentContentPage.applyToolbarCommand(`Line ${i}`, 'btnBulletedList')
|
||||||
|
}
|
||||||
|
await documentContentPage.addSeparator('Line 15')
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 15', 'btnH3')
|
||||||
|
await documentContentPage.goToByTOC('Line 15')
|
||||||
|
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 16', 'btnBlockquote')
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 17', 'btnCode')
|
||||||
|
await documentContentPage.applyToolbarCommand('Line 18', 'btnCodeBlock')
|
||||||
|
await documentContentPage.changeCodeBlockLanguage('Line 18', 'auto', 'css')
|
||||||
|
await documentContentPage.applyNote('Line 19', 'warning', testNote)
|
||||||
|
await documentContentPage.addImage('Line 20')
|
||||||
|
await page.keyboard.type('Cat')
|
||||||
|
|
||||||
|
newUser2 = generateUser()
|
||||||
|
await createAccount(request, newUser2)
|
||||||
|
const linkText = await getInviteLink(page)
|
||||||
|
using _secondPage = await getSecondPageByInvite(browser, linkText, newUser2)
|
||||||
|
secondPage = _secondPage.page
|
||||||
|
leftSideMenuSecondPage = new LeftSideMenuPage(secondPage)
|
||||||
|
documentsSecondPage = new DocumentsPage(secondPage)
|
||||||
|
documentContentSecondPage = new DocumentContentPage(secondPage)
|
||||||
|
|
||||||
|
await leftSideMenuSecondPage.clickDocuments()
|
||||||
|
await documentsSecondPage.openTeamspace(testDocument.space)
|
||||||
|
await documentsSecondPage.openDocument(testDocument.title)
|
||||||
|
await documentContentSecondPage.checkDocumentTitle(testDocument.title)
|
||||||
|
await documentContentSecondPage.checkLinkInTheText('Line 8', testLink)
|
||||||
|
await documentContentSecondPage.goToByTOC('Line 15')
|
||||||
|
await documentContentSecondPage.checkImage()
|
||||||
|
await documentContentSecondPage.checkNote('Line 19', 'warning', testNote)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -169,7 +169,7 @@ test.describe('Documents tests', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Add Link to the Document', async () => {
|
test.skip('Add Link to the Document', async () => {
|
||||||
const contentLink = 'Lineforthelink'
|
const contentLink = 'Lineforthelink'
|
||||||
const linkDocument: NewDocument = {
|
const linkDocument: NewDocument = {
|
||||||
title: `Links Document Title-${generateId()}`,
|
title: `Links Document Title-${generateId()}`,
|
||||||
|
@ -206,6 +206,10 @@ export class CommonPage {
|
|||||||
await expect(this.menuPopupItemButton(itemText)).toBeVisible()
|
await expect(this.menuPopupItemButton(itemText)).toBeVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clickPopupItem (itemText: string): Promise<void> {
|
||||||
|
await this.menuPopupItemButton(itemText).first().click()
|
||||||
|
}
|
||||||
|
|
||||||
async selectFilter (filter: string, filterSecondLevel?: string): Promise<void> {
|
async selectFilter (filter: string, filterSecondLevel?: string): Promise<void> {
|
||||||
await this.buttonFilter().click()
|
await this.buttonFilter().click()
|
||||||
await this.selectPopupMenu(filter).click()
|
await this.selectPopupMenu(filter).click()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { type Locator, type Page, expect } from '@playwright/test'
|
import { type Locator, type Page, expect } from '@playwright/test'
|
||||||
import { CommonPage } from '../common-page'
|
import { CommonPage } from '../common-page'
|
||||||
import { uploadFile } from '../../utils'
|
import { uploadFile } from '../../utils'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
export class DocumentContentPage extends CommonPage {
|
export class DocumentContentPage extends CommonPage {
|
||||||
readonly page: Page
|
readonly page: Page
|
||||||
@ -35,7 +36,9 @@ export class DocumentContentPage extends CommonPage {
|
|||||||
readonly buttonInsertInnerRow = (row: number = 0): Locator =>
|
readonly buttonInsertInnerRow = (row: number = 0): Locator =>
|
||||||
this.page.locator('table.proseTable').locator('tr').nth(row).locator('div.table-row-insert button')
|
this.page.locator('table.proseTable').locator('tr').nth(row).locator('div.table-row-insert button')
|
||||||
|
|
||||||
readonly buttonToolbarLink = (): Locator => this.page.locator('div.text-editor-toolbar button[data-id="btnLink"]')
|
readonly buttonOnToolbar = (id: string): Locator =>
|
||||||
|
this.page.locator(`div.text-editor-toolbar button[data-id="${id}"]`)
|
||||||
|
|
||||||
readonly inputFormLink = (): Locator => this.page.locator('form[id="text-editor:string:Link"] input')
|
readonly inputFormLink = (): Locator => this.page.locator('form[id="text-editor:string:Link"] input')
|
||||||
readonly buttonFormLinkSave = (): Locator =>
|
readonly buttonFormLinkSave = (): Locator =>
|
||||||
this.page.locator('form[id="text-editor:string:Link"] button[type="submit"]')
|
this.page.locator('form[id="text-editor:string:Link"] button[type="submit"]')
|
||||||
@ -61,6 +64,18 @@ export class DocumentContentPage extends CommonPage {
|
|||||||
|
|
||||||
readonly slashActionItemsPopup = (): Locator => this.page.locator('.selectPopup')
|
readonly slashActionItemsPopup = (): Locator => this.page.locator('.selectPopup')
|
||||||
|
|
||||||
|
readonly codeBlock = (hasText: string): Locator => this.page.locator('pre.proseCodeBlock > code', { hasText })
|
||||||
|
readonly inputFormNote = (): Locator => this.page.locator('form[id="text-editor:string:ConfigureNote"] textarea')
|
||||||
|
readonly colorFormNote = (color: string): Locator =>
|
||||||
|
this.page.locator(`form[id="text-editor:string:ConfigureNote"] div.colorBox.${color}`)
|
||||||
|
|
||||||
|
readonly setFormNote = (): Locator =>
|
||||||
|
this.page.locator('form[id="text-editor:string:ConfigureNote"] div.antiCard-footer button[type="submit"]')
|
||||||
|
|
||||||
|
readonly inputImageFile = (): Locator => this.page.locator('input[id="imageInput"]')
|
||||||
|
readonly imageInContent = (): Locator => this.page.locator('p img[data-type="image"]')
|
||||||
|
readonly noteInContent = (hasText: string): Locator => this.page.locator('p span[data-mark="note"]', { hasText })
|
||||||
|
|
||||||
async checkDocumentTitle (title: string): Promise<void> {
|
async checkDocumentTitle (title: string): Promise<void> {
|
||||||
await expect(this.buttonDocumentTitle()).toHaveValue(title)
|
await expect(this.buttonDocumentTitle()).toHaveValue(title)
|
||||||
}
|
}
|
||||||
@ -69,10 +84,10 @@ export class DocumentContentPage extends CommonPage {
|
|||||||
await expect(this.buttonLockedInTitle()).toBeVisible({ timeout: 1000 })
|
await expect(this.buttonLockedInTitle()).toBeVisible({ timeout: 1000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async addContentToTheNewLine (newContent: string): Promise<string> {
|
async addContentToTheNewLine (newContent: string, newLine: boolean = true): Promise<string> {
|
||||||
await expect(this.inputContent()).toBeVisible()
|
await expect(this.inputContent()).toBeVisible()
|
||||||
await expect(this.inputContent()).toHaveJSProperty('contentEditable', 'true')
|
await expect(this.inputContent()).toHaveJSProperty('contentEditable', 'true')
|
||||||
await this.inputContent().pressSequentially(`\n${newContent}`)
|
await this.inputContent().pressSequentially(`${newLine ? '\n' : ''}${newContent}`)
|
||||||
const endContent = await this.inputContent().textContent()
|
const endContent = await this.inputContent().textContent()
|
||||||
return endContent ?? ''
|
return endContent ?? ''
|
||||||
}
|
}
|
||||||
@ -202,14 +217,70 @@ export class DocumentContentPage extends CommonPage {
|
|||||||
|
|
||||||
async addLinkToText (text: string, link: string): Promise<void> {
|
async addLinkToText (text: string, link: string): Promise<void> {
|
||||||
await expect(this.page.locator('p', { hasText: text })).toBeVisible()
|
await expect(this.page.locator('p', { hasText: text })).toBeVisible()
|
||||||
await this.page.locator('p', { hasText: text }).click()
|
await this.page.locator('p', { hasText: text }).click({ clickCount: 3 })
|
||||||
await this.page.locator('p', { hasText: text }).dblclick()
|
await this.buttonOnToolbar('btnLink').click()
|
||||||
await this.buttonToolbarLink().click()
|
|
||||||
|
|
||||||
await this.inputFormLink().fill(link)
|
await this.inputFormLink().fill(link)
|
||||||
await this.buttonFormLinkSave().click()
|
await this.buttonFormLinkSave().click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clickButtonOnTooltip (id: string): Promise<void> {
|
||||||
|
await this.buttonOnToolbar(id).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectLine (text: string): Promise<void> {
|
||||||
|
const loc: Locator = this.page.locator('p', { hasText: text }).first()
|
||||||
|
await expect(loc).toBeVisible()
|
||||||
|
await loc.click({ clickCount: 3 })
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyToolbarCommand (text: string, btnId: string): Promise<void> {
|
||||||
|
await this.selectLine(text)
|
||||||
|
await this.clickButtonOnTooltip(btnId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async addSeparator (text: string): Promise<void> {
|
||||||
|
await this.selectLine(text)
|
||||||
|
await this.clickLeftMenu()
|
||||||
|
await this.clickPopupItem('Separator line')
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToByTOC (text: string): Promise<void> {
|
||||||
|
await this.tocItems().first().click()
|
||||||
|
await this.clickPopupItem(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickLeftMenu (): Promise<void> {
|
||||||
|
await this.leftMenu().click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeCodeBlockLanguage (text: string, oldLang: string, lang: string): Promise<void> {
|
||||||
|
await this.codeBlock(text).locator('button.antiButton', { hasText: oldLang }).nth(1).click()
|
||||||
|
await this.selectMenuItem(this.page, lang, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyNote (text: string, color: string, note: string): Promise<void> {
|
||||||
|
await this.applyToolbarCommand(text, 'btnNote')
|
||||||
|
await this.inputFormNote().fill(note)
|
||||||
|
await this.colorFormNote(color).click()
|
||||||
|
await this.setFormNote().click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkNote (text: string, color: string, note: string): Promise<void> {
|
||||||
|
await expect(this.noteInContent(text)).toBeVisible()
|
||||||
|
await expect(this.noteInContent(text)).toHaveAttribute('data-kind', color)
|
||||||
|
await expect(this.noteInContent(text)).toHaveAttribute('title', note)
|
||||||
|
}
|
||||||
|
|
||||||
|
async addImage (text: string): Promise<void> {
|
||||||
|
await this.selectLine(text)
|
||||||
|
await this.inputImageFile().setInputFiles(path.join(__dirname, '../../files/cat.jpeg'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkImage (width: number = 215): Promise<void> {
|
||||||
|
await expect(this.imageInContent()).toHaveAttribute('width', width.toString())
|
||||||
|
}
|
||||||
|
|
||||||
async checkLinkInTheText (text: string, link: string): Promise<void> {
|
async checkLinkInTheText (text: string, link: string): Promise<void> {
|
||||||
await expect(this.page.locator('a', { hasText: text })).toHaveAttribute('href', link)
|
await expect(this.page.locator('a', { hasText: text })).toHaveAttribute('href', link)
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,10 @@ export class TalentDetailsPage extends CommonRecruitingPage {
|
|||||||
await this.buttonFinalContact().click()
|
await this.buttonFinalContact().click()
|
||||||
await this.selectMenuItem(this.page, talentName.finalContactName)
|
await this.selectMenuItem(this.page, talentName.finalContactName)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
this.buttonMergeRow().locator('div.flex-center', { hasText: talentName.name }).locator('label.checkbox-container')
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
await this.buttonMergeRow()
|
await this.buttonMergeRow()
|
||||||
.locator('div.flex-center', { hasText: talentName.name })
|
.locator('div.flex-center', { hasText: talentName.name })
|
||||||
.locator('label.checkbox-container')
|
.locator('label.checkbox-container')
|
||||||
|
Loading…
Reference in New Issue
Block a user