mirror of
https://github.com/hcengineering/platform.git
synced 2025-03-20 22:24:14 +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:
|
||||
specifier: ^4.56.0
|
||||
version: 4.56.0(zod@3.23.8)
|
||||
openid-client:
|
||||
specifier: ~5.7.0
|
||||
version: 5.7.0
|
||||
otp-generator:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@ -17193,6 +17196,10 @@ packages:
|
||||
engines: {node: '>= 0.6.0'}
|
||||
dev: false
|
||||
|
||||
/jose@4.15.9:
|
||||
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
|
||||
dev: false
|
||||
|
||||
/jose@5.6.3:
|
||||
resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==}
|
||||
dev: false
|
||||
@ -19076,6 +19083,11 @@ packages:
|
||||
resolution: {integrity: sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ==}
|
||||
dev: false
|
||||
|
||||
/object-hash@2.2.0:
|
||||
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
|
||||
engines: {node: '>= 6'}
|
||||
dev: false
|
||||
|
||||
/object-hash@3.0.0:
|
||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -19210,6 +19222,11 @@ packages:
|
||||
resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
|
||||
dev: false
|
||||
@ -19298,6 +19315,15 @@ packages:
|
||||
hasBin: true
|
||||
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:
|
||||
resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==}
|
||||
dev: false
|
||||
@ -25220,7 +25246,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
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
|
||||
name: '@rush-temp/auth-providers'
|
||||
version: 0.0.0
|
||||
@ -25248,6 +25274,7 @@ packages:
|
||||
koa-router: 12.0.1
|
||||
koa-session: 6.4.0
|
||||
mongodb: 6.9.0
|
||||
openid-client: 5.7.0
|
||||
passport-custom: 1.1.1
|
||||
passport-github2: 0.1.12
|
||||
passport-google-oauth20: 2.0.0
|
||||
|
@ -152,6 +152,9 @@ export class TActivityReference extends TActivityMessage implements ActivityRefe
|
||||
@Prop(TypeMarkup(), activity.string.Message)
|
||||
@Index(IndexKind.FullText)
|
||||
message!: string
|
||||
|
||||
@Prop(TypeTimestamp(), activity.string.Edit)
|
||||
editedOn?: Timestamp
|
||||
}
|
||||
|
||||
@Model(activity.class.ActivityInfoMessage, activity.class.ActivityMessage)
|
||||
|
@ -22,7 +22,6 @@ import {
|
||||
TypeMarkup,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
TypeTimestamp,
|
||||
UX
|
||||
} from '@hcengineering/model'
|
||||
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
||||
@ -59,6 +58,7 @@ import type { DocNotifyContext } from '@hcengineering/notification'
|
||||
import chunter from './plugin'
|
||||
|
||||
export const DOMAIN_CHUNTER = 'chunter' as Domain
|
||||
|
||||
@Model(chunter.class.ChunterSpace, core.class.Space)
|
||||
export class TChunterSpace extends TSpace implements ChunterSpace {
|
||||
@Prop(PropCollection(activity.class.ActivityMessage), chunter.string.Messages)
|
||||
@ -84,9 +84,6 @@ export class TChatMessage extends TActivityMessage implements ChatMessage {
|
||||
@Index(IndexKind.FullText)
|
||||
message!: string
|
||||
|
||||
@Prop(TypeTimestamp(), chunter.string.Edit)
|
||||
editedOn?: Timestamp
|
||||
|
||||
@Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, {
|
||||
shortLabel: attachment.string.Files
|
||||
})
|
||||
|
@ -562,6 +562,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
||||
input: 'focus',
|
||||
category: tracker.category.Tracker,
|
||||
target: tracker.class.Issue,
|
||||
visibilityTester: view.function.IsClipboardAvailable,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
application: tracker.app.Tracker,
|
||||
@ -582,6 +583,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
||||
input: 'focus',
|
||||
category: tracker.category.Tracker,
|
||||
target: tracker.class.Issue,
|
||||
visibilityTester: view.function.IsClipboardAvailable,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
application: tracker.app.Tracker,
|
||||
@ -602,6 +604,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
||||
input: 'focus',
|
||||
category: tracker.category.Tracker,
|
||||
target: tracker.class.Issue,
|
||||
visibilityTester: view.function.IsClipboardAvailable,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
application: tracker.app.Tracker,
|
||||
|
@ -140,6 +140,7 @@ export default mergeIds(viewId, view, {
|
||||
CanDeleteSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
CanJoinSpace: '' 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>>,
|
||||
BlobVideoMetadata: '' as Resource<(file: FileOrBlob, blob: Ref<Blob>) => Promise<BlobMetadata | undefined>>
|
||||
},
|
||||
|
@ -321,6 +321,7 @@
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: var(--spacing-6_5);
|
||||
overflow: hidden;
|
||||
|
||||
&.clearPadding > .hulyHeader-row {
|
||||
padding: 0;
|
||||
@ -577,9 +578,12 @@
|
||||
}
|
||||
&.second:not(.isOpen),
|
||||
&.border,
|
||||
&.default:not(.nested:last-child) {
|
||||
&.default:not(.nested) {
|
||||
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 {
|
||||
display: flex;
|
||||
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 {
|
||||
color: var(--button-subtle-IconColor);
|
||||
background-color: var(--global-ui-hover-BackgroundColor);
|
||||
@ -806,6 +816,9 @@
|
||||
&.medium.bottomSpace + .hulyAccordionItem-content {
|
||||
padding-bottom: var(--spacing-2);
|
||||
}
|
||||
&.medium.nested.bottomSpace + .hulyAccordionItem-content {
|
||||
padding-bottom: var(--spacing-1);
|
||||
}
|
||||
&.large.bottomSpace + .hulyAccordionItem-content {
|
||||
padding-bottom: var(--spacing-2);
|
||||
}
|
||||
|
@ -39,6 +39,7 @@
|
||||
export let duration: number | boolean = false
|
||||
export let fixHeader: boolean = false
|
||||
export let categoryHeader: boolean = false
|
||||
export let hiddenHeader: boolean = false
|
||||
export let background: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -74,6 +75,7 @@
|
||||
class:selectable
|
||||
class:scroller-header={fixHeader}
|
||||
class:categoryHeader
|
||||
class:hiddenHeader
|
||||
style:background-color={background ?? 'transparent'}
|
||||
on:click|stopPropagation={handleClick}
|
||||
>
|
||||
|
@ -39,6 +39,7 @@
|
||||
export let noPrint: boolean = false
|
||||
export let freezeBefore: boolean = false
|
||||
export let doubleRowWidth = 768
|
||||
export let closeOnEscape: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -61,7 +62,7 @@
|
||||
})
|
||||
|
||||
function _close (ev: KeyboardEvent): void {
|
||||
if (closeButton && ev.key === 'Escape') {
|
||||
if (closeButton && ev.key === 'Escape' && closeOnEscape) {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
|
||||
@ -138,7 +139,9 @@
|
||||
{/if}
|
||||
{#if closeButton}
|
||||
{#if type !== 'type-popup'}<div class="hulyHeader-divider no-print" />{/if}
|
||||
<div class="hulyHotKey-item no-print">Esc</div>
|
||||
{#if closeOnEscape}
|
||||
<div class="hulyHotKey-item no-print">Esc</div>
|
||||
{/if}
|
||||
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} noPrint on:click={() => dispatch('close')} />
|
||||
{/if}
|
||||
</div>
|
||||
@ -233,7 +236,9 @@
|
||||
{/if}
|
||||
{#if closeButton}
|
||||
{#if type !== 'type-popup'}<div class="hulyHeader-divider no-print" />{/if}
|
||||
<div class="hulyHotKey-item no-print">Esc</div>
|
||||
{#if closeOnEscape}
|
||||
<div class="hulyHotKey-item no-print">Esc</div>
|
||||
{/if}
|
||||
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} noPrint on:click={() => dispatch('close')} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -85,6 +85,7 @@
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
color: var(--theme-content-color);
|
||||
|
||||
&.primary {
|
||||
background-color: var(--theme-button-pressed);
|
||||
|
@ -126,6 +126,7 @@ export interface TabItem {
|
||||
}
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
id?: string
|
||||
icon?: Asset | AnySvelteComponent | ComponentType
|
||||
iconProps?: any
|
||||
iconWidth?: string
|
||||
|
@ -26,6 +26,7 @@
|
||||
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import { Action as ViewAction } from '@hcengineering/view'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
|
||||
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
|
||||
@ -228,6 +229,9 @@
|
||||
<span class="text-sm lower">
|
||||
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
|
||||
</span>
|
||||
{#if message.editedOn}
|
||||
<span class="text-sm lower">(<Label label={notification.string.Edited} />)</span>
|
||||
{/if}
|
||||
|
||||
{#if withActions && inlineActions.length > 0 && !readonly}
|
||||
<div class="flex-presenter flex-gap-2 ml-2">
|
||||
|
@ -46,6 +46,7 @@ export interface ActivityMessage extends AttachedDoc {
|
||||
|
||||
replies?: number
|
||||
reactions?: number
|
||||
editedOn?: Timestamp
|
||||
}
|
||||
|
||||
export type DisplayActivityMessage = DisplayDocUpdateMessage | ActivityMessage
|
||||
@ -81,6 +82,7 @@ export interface ActivityInfoMessage extends ActivityMessage {
|
||||
props?: Record<string, any>
|
||||
icon?: Asset
|
||||
iconProps?: Record<string, any>
|
||||
editedOn?: Timestamp
|
||||
|
||||
// A possible set of links to some platform resources.
|
||||
links?: { _class: Ref<Class<Doc>>, _id: Ref<Doc> }[]
|
||||
|
@ -42,6 +42,7 @@
|
||||
hideExtra={false}
|
||||
adaptive="autoExtra"
|
||||
doubleRowWidth={350}
|
||||
closeOnEscape={false}
|
||||
on:close
|
||||
>
|
||||
<div class="title">
|
||||
|
@ -30,6 +30,7 @@
|
||||
export let filters: Ref<ActivityMessagesFilter>[] = []
|
||||
export let isAsideOpened = false
|
||||
export let syncLocation = true
|
||||
export let freeze = false
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -111,5 +112,6 @@
|
||||
provider={dataProvider}
|
||||
{isAsideOpened}
|
||||
loadMoreAllowed={!isDocChannel}
|
||||
{freeze}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -36,6 +36,7 @@
|
||||
export let isAsideShown: boolean = false
|
||||
export let filters: Ref<ActivityMessagesFilter>[] = []
|
||||
export let canOpenInSidebar: boolean = false
|
||||
export let closeOnEscape: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -80,6 +81,7 @@
|
||||
{isAsideShown}
|
||||
{withSearch}
|
||||
{canOpenInSidebar}
|
||||
{closeOnEscape}
|
||||
on:aside-toggled
|
||||
on:close
|
||||
>
|
||||
|
@ -61,8 +61,9 @@
|
||||
export let skipLabels = false
|
||||
export let loadMoreAllowed = true
|
||||
export let isAsideOpened = false
|
||||
export let initialScrollBottom = true
|
||||
export let fullHeight = true
|
||||
export let fixedInput = true
|
||||
export let freeze = false
|
||||
|
||||
const doc = object
|
||||
|
||||
@ -130,17 +131,21 @@
|
||||
}
|
||||
})
|
||||
|
||||
function isFreeze (): boolean {
|
||||
return freeze
|
||||
}
|
||||
|
||||
$: displayMessages = filterChatMessages(messages, filters, filterResources, doc._class, selectedFilters)
|
||||
|
||||
const unsubscribe = inboxClient.inboxNotificationsByContext.subscribe(() => {
|
||||
if (notifyContext !== undefined) {
|
||||
if (notifyContext !== undefined && !isFreeze()) {
|
||||
recheckNotifications(notifyContext)
|
||||
readViewportMessages()
|
||||
}
|
||||
})
|
||||
|
||||
function scrollToBottom (afterScrollFn?: () => void): void {
|
||||
if (scroller != null && scrollElement != null) {
|
||||
if (scroller != null && scrollElement != null && !isFreeze()) {
|
||||
scroller.scrollBy(scrollElement.scrollHeight)
|
||||
updateSelectedDate()
|
||||
afterScrollFn?.()
|
||||
@ -338,7 +343,7 @@
|
||||
let messagesToReadAccumulatorTimer: any
|
||||
|
||||
function readViewportMessages (): void {
|
||||
if (!scrollElement || !scrollContentBox) {
|
||||
if (!scrollElement || !scrollContentBox || isFreeze()) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -452,22 +457,14 @@
|
||||
isInitialScrolling = false
|
||||
} else if (separatorIndex === -1) {
|
||||
await wait()
|
||||
if (initialScrollBottom) {
|
||||
isScrollInitialized = true
|
||||
shouldWaitAndRead = true
|
||||
autoscroll = true
|
||||
shouldScrollToNew = true
|
||||
isInitialScrolling = false
|
||||
waitLastMessageRenderAndRead(() => {
|
||||
autoscroll = false
|
||||
})
|
||||
} else {
|
||||
isScrollInitialized = true
|
||||
isScrollInitialized = true
|
||||
shouldWaitAndRead = true
|
||||
autoscroll = true
|
||||
shouldScrollToNew = true
|
||||
isInitialScrolling = false
|
||||
waitLastMessageRenderAndRead(() => {
|
||||
autoscroll = false
|
||||
updateShouldScrollToNew()
|
||||
isInitialScrolling = false
|
||||
readViewportMessages()
|
||||
}
|
||||
})
|
||||
} else if (separatorElement) {
|
||||
await wait()
|
||||
scrollToSeparator()
|
||||
@ -519,6 +516,7 @@
|
||||
|
||||
function scrollToNewMessages (): void {
|
||||
if (!scrollElement || !shouldScrollToNew) {
|
||||
readViewportMessages()
|
||||
return
|
||||
}
|
||||
|
||||
@ -557,14 +555,22 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (isFreeze()) {
|
||||
messagesCount = newCount
|
||||
return
|
||||
}
|
||||
|
||||
if (scrollToRestore > 0) {
|
||||
void restoreScroll()
|
||||
} else if (dateToJump !== undefined) {
|
||||
await wait()
|
||||
scrollToDate(dateToJump)
|
||||
} else if (messagesCount > 0 && newCount > messagesCount) {
|
||||
} else if (shouldScrollToNew && messagesCount > 0 && newCount > messagesCount) {
|
||||
await wait()
|
||||
scrollToNewMessages()
|
||||
} else {
|
||||
await wait()
|
||||
readViewportMessages()
|
||||
}
|
||||
|
||||
messagesCount = newCount
|
||||
@ -576,7 +582,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldScrollToNew && initialScrollBottom) {
|
||||
if (shouldScrollToNew) {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
@ -660,7 +666,7 @@
|
||||
} else if (element != null) {
|
||||
const { scrollHeight, scrollTop, offsetHeight } = element
|
||||
|
||||
showScrollDownButton = scrollHeight > offsetHeight + scrollTop + 300
|
||||
showScrollDownButton = scrollHeight > offsetHeight + scrollTop + 50
|
||||
} else {
|
||||
showScrollDownButton = false
|
||||
}
|
||||
@ -691,7 +697,7 @@
|
||||
$: void forceReadContext(isScrollAtBottom, notifyContext)
|
||||
|
||||
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
|
||||
|
||||
if (lastViewedTimestamp >= lastUpdateTimestamp) return
|
||||
@ -708,6 +714,10 @@
|
||||
}
|
||||
|
||||
const canLoadNextForwardStore = provider.canLoadNextForwardStore
|
||||
|
||||
$: if (!freeze) {
|
||||
readViewportMessages()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
@ -777,6 +787,16 @@
|
||||
/>
|
||||
{/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}
|
||||
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
||||
{/if}
|
||||
@ -794,7 +814,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if object}
|
||||
{#if fixedInput && object}
|
||||
<div class="ref-input flex-col">
|
||||
<ActivityExtensionComponent
|
||||
kind="input"
|
||||
@ -841,7 +861,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
bottom: -0.75rem;
|
||||
animation: 1s fadeIn;
|
||||
animation: 0.5s fadeIn;
|
||||
animation-fill-mode: forwards;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
@ -58,9 +58,14 @@
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
let renderChannel = tab.data.thread === undefined
|
||||
|
||||
$: if (tab.data.thread === undefined) {
|
||||
renderChannel = true
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
{#if object && renderChannel}
|
||||
<div class="channel" class:invisible={threadId !== undefined} style:height style:width>
|
||||
<ChannelHeader
|
||||
_id={object._id}
|
||||
@ -71,16 +76,17 @@
|
||||
canOpen={true}
|
||||
allowClose={true}
|
||||
canOpenInSidebar={false}
|
||||
closeOnEscape={false}
|
||||
on:close
|
||||
/>
|
||||
{#key object._id}
|
||||
<Channel {object} {context} syncLocation={false} />
|
||||
<Channel {object} {context} syncLocation={false} freeze={threadId !== undefined} />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{#if threadId}
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
|
@ -93,7 +93,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="popupPanel panel">
|
||||
<div class="popupPanel">
|
||||
<ChannelHeader
|
||||
_id={object._id}
|
||||
_class={object._class}
|
||||
|
@ -36,7 +36,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if widget && tab && tab.type === 'channel'}
|
||||
{#if widget && tab}
|
||||
<ChannelSidebarView
|
||||
{widget}
|
||||
{tab}
|
||||
@ -46,13 +46,4 @@
|
||||
handleClose(tab?.id)
|
||||
}}
|
||||
/>
|
||||
{:else if widget && tab && tab.type === 'thread'}
|
||||
<ThreadSidebarView
|
||||
{tab}
|
||||
{height}
|
||||
{width}
|
||||
on:close={() => {
|
||||
handleClose(tab?.id)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -50,7 +50,7 @@
|
||||
|
||||
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
|
||||
|
||||
const unsubscribe = notificationClient.inboxNotificationsByContext.subscribe((res) => {
|
||||
|
@ -57,6 +57,7 @@
|
||||
export let adaptive: HeaderAdaptive = 'default'
|
||||
export let hideActions: boolean = false
|
||||
export let canOpenInSidebar: boolean = false
|
||||
export let closeOnEscape: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -72,6 +73,7 @@
|
||||
hideActions={!((canOpen && object) || withAside || $$slots.actions) || hideActions}
|
||||
hideDescription={!description}
|
||||
adaptive={adaptive !== 'default' ? adaptive : withFilters ? 'freezeActions' : 'disabled'}
|
||||
{closeOnEscape}
|
||||
on:click
|
||||
on:close
|
||||
>
|
||||
|
@ -33,7 +33,7 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={div}
|
||||
class="border-radius-4 over-underline dateSelectorButton clear-mins"
|
||||
class="border-radius-4 dateSelectorButton clear-mins"
|
||||
on:click={() => {
|
||||
showPopup(DateRangePopup, {}, div, (v) => {
|
||||
if (v) {
|
||||
@ -68,13 +68,17 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--theme-divider-color);
|
||||
background-color: var(--highlight-select-border);
|
||||
}
|
||||
|
||||
.dateSelectorButton {
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
height: max-content;
|
||||
background-color: var(--theme-list-row-color);
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
color: var(--theme-content-color);
|
||||
background-color: var(--highlight-select);
|
||||
border: 1px solid var(--highlight-select-border);
|
||||
font-weight: 500;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
@ -14,20 +14,15 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Timestamp } from '@hcengineering/core'
|
||||
|
||||
export let editedOn: Timestamp | undefined
|
||||
export let label: IntlString | undefined
|
||||
</script>
|
||||
|
||||
{#if label}
|
||||
<span class="text-sm lower"> <Label {label} /></span>
|
||||
{/if}
|
||||
{#if editedOn}
|
||||
<span class="text-sm lower"><Label label={notification.string.Edited} /></span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
|
@ -255,7 +255,7 @@
|
||||
{onClick}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<ChatMessageHeader editedOn={value.editedOn} label={skipLabel ? undefined : viewlet?.label} />
|
||||
<ChatMessageHeader label={viewlet?.label} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if !isEditing}
|
||||
|
@ -76,7 +76,7 @@
|
||||
|
||||
if (direct !== undefined && missingAccounts.length > 0) {
|
||||
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}
|
||||
<div class="root" style:height style:width>
|
||||
<ThreadView _id={tab.data.thread} on:close />
|
||||
<ThreadView _id={tab.data.thread} on:close on:channel />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
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 activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import { getMessageFromLoc, messageInFocus } from '@hcengineering/activity-resources'
|
||||
@ -104,7 +104,14 @@
|
||||
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) {
|
||||
return []
|
||||
}
|
||||
@ -115,6 +122,7 @@
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'channel',
|
||||
icon: getObjectIcon(message.attachedToClass),
|
||||
iconProps: { value: channel },
|
||||
iconWidth: isPersonAvatar ? 'auto' : undefined,
|
||||
@ -122,14 +130,24 @@
|
||||
title: channelName,
|
||||
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>
|
||||
|
||||
{#if showHeader}
|
||||
<Header type={'type-aside'} adaptive={'disabled'} on:close>
|
||||
<Breadcrumbs items={getBreadcrumbsItems(channel, message, channelName)} currentOnly />
|
||||
<Header type={'type-aside'} adaptive={'disabled'} closeOnEscape={false} on:close>
|
||||
<Breadcrumbs items={breadcrumbs} on:select={handleBreadcrumbSelect} selected={1} />
|
||||
</Header>
|
||||
{/if}
|
||||
|
||||
@ -141,8 +159,8 @@
|
||||
skipLabels
|
||||
object={message}
|
||||
provider={dataProvider}
|
||||
initialScrollBottom={false}
|
||||
fullHeight={false}
|
||||
fixedInput={false}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="mt-3">
|
||||
|
@ -61,6 +61,7 @@ import {
|
||||
getThreadLink,
|
||||
openChannelInSidebar,
|
||||
openChannelInSidebarAction,
|
||||
openThreadInSidebar,
|
||||
replyToThread
|
||||
} from './navigation'
|
||||
import {
|
||||
@ -199,7 +200,8 @@ export default async (): Promise<Resources> => ({
|
||||
GetMessageLink: getMessageLocation,
|
||||
CloseChatWidgetTab: closeChatWidgetTab,
|
||||
OpenChannelInSidebar: openChannelInSidebar,
|
||||
CanTranslateMessage: canTranslateMessage
|
||||
CanTranslateMessage: canTranslateMessage,
|
||||
OpenThreadInSidebar: openThreadInSidebar
|
||||
},
|
||||
actionImpl: {
|
||||
ArchiveChannel,
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
type Location,
|
||||
navigate
|
||||
} 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 {
|
||||
type Channel,
|
||||
@ -262,11 +262,11 @@ export async function openChannelInSidebar (
|
||||
size: isDirect || isPerson ? 'tiny' : 'x-small',
|
||||
compact: true
|
||||
},
|
||||
type: 'channel',
|
||||
data: {
|
||||
_id,
|
||||
_class,
|
||||
thread
|
||||
thread,
|
||||
channelName: name
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,18 +288,26 @@ export async function openThreadInSidebarChannel (
|
||||
): Promise<void> {
|
||||
const newTab: ChatWidgetTab = {
|
||||
...tab,
|
||||
name: await translate(chunter.string.ThreadIn, { name: tab.data.channelName }),
|
||||
data: { ...tab.data, thread: message._id }
|
||||
}
|
||||
createWidgetTab(widget, newTab)
|
||||
}
|
||||
|
||||
export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidgetTab): Promise<void> {
|
||||
const thread = tab.allowedPath !== undefined ? tab.data.thread : undefined
|
||||
const newTab: ChatWidgetTab = {
|
||||
...tab,
|
||||
id: tab.id.startsWith('thread_') ? generateId() : tab.id,
|
||||
name: tab.data.channelName,
|
||||
allowedPath: undefined,
|
||||
data: { ...tab.data, thread: undefined }
|
||||
}
|
||||
|
||||
createWidgetTab(widget, newTab)
|
||||
setTimeout(() => {
|
||||
removeThreadFromLoc(thread)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
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 currentTAbs = get(sidebarStore).widgetsState.get(widget._id)?.tabs ?? []
|
||||
const tabsToClose = currentTAbs
|
||||
.filter((t) => t.isPinned !== true && t.allowedPath === allowedPath && (t as ChatWidgetTab).type === 'thread')
|
||||
.map((t) => t.id)
|
||||
const tabsToClose = currentTAbs.filter((t) => t.isPinned !== true && t.allowedPath === allowedPath).map((t) => t.id)
|
||||
|
||||
if (tabsToClose.length > 0) {
|
||||
sidebarStore.update((s) => {
|
||||
@ -341,26 +347,31 @@ export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: Acti
|
||||
name: tabName,
|
||||
icon: chunter.icon.Thread,
|
||||
allowedPath,
|
||||
type: 'thread',
|
||||
data: {
|
||||
_id: object?._id,
|
||||
_class: object?._class,
|
||||
thread: message._id
|
||||
thread: message._id,
|
||||
channelName: name
|
||||
}
|
||||
}
|
||||
createWidgetTab(widget, tab, true)
|
||||
}
|
||||
|
||||
export function closeChatWidgetTab (tab?: ChatWidgetTab): void {
|
||||
if (tab?.type === 'thread') {
|
||||
const loc = getCurrentLocation()
|
||||
export function removeThreadFromLoc (thread?: Ref<ActivityMessage>): void {
|
||||
if (thread === undefined) return
|
||||
const loc = getCurrentLocation()
|
||||
|
||||
if (loc.path[2] === chunterId || loc.path[2] === notificationId) {
|
||||
if (loc.path[4] === tab.data.thread) {
|
||||
loc.path[4] = ''
|
||||
loc.path.length = 4
|
||||
navigate(loc)
|
||||
}
|
||||
if (loc.path[2] === chunterId || loc.path[2] === notificationId) {
|
||||
if (loc.path[4] === thread) {
|
||||
loc.path[4] = ''
|
||||
loc.path.length = 4
|
||||
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
|
||||
}
|
||||
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
if (context === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
const client = getClient().apply(undefined, 'readViewportMessages')
|
||||
|
||||
try {
|
||||
const readMessages = get(chatReadMessagesStore)
|
||||
const allIds = getAllIds(messages).filter((id) => !readMessages.has(id))
|
||||
@ -460,12 +464,6 @@ export async function readChannelMessages (
|
||||
|
||||
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 newTimestamp = messages[messages.length - 1].createdOn ?? 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 inboxClient.readNotifications(client, [...notifications, ...relatedMentions])
|
||||
} finally {
|
||||
await client.commit()
|
||||
}
|
||||
|
@ -100,8 +100,7 @@ export interface InlineButton extends AttachedDoc {
|
||||
}
|
||||
|
||||
export interface ChatWidgetTab extends WidgetTab {
|
||||
type: 'channel' | 'thread'
|
||||
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage> }
|
||||
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>, channelName: string }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -232,6 +231,7 @@ export default plugin(chunterId, {
|
||||
},
|
||||
function: {
|
||||
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
OpenThreadInSidebar: '' as Resource<(_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc) => Promise<void>>,
|
||||
OpenChannelInSidebar: '' as Resource<
|
||||
(_id: Ref<Doc>, _class: Ref<Doc>, doc?: Doc, thread?: Ref<ActivityMessage>) => Promise<void>
|
||||
>
|
||||
|
@ -32,13 +32,13 @@
|
||||
{#if value !== undefined}
|
||||
<div class:highlighted class="root">
|
||||
<div class="header pt-2 pb-2 pl-4 pr-4 flex-between">
|
||||
<div>
|
||||
<span class="overflow-label">
|
||||
{#if value?.index}
|
||||
<span>#{value.index}</span>
|
||||
<span data-id="commentId">#{value.index}</span>
|
||||
<span>•</span>
|
||||
{/if}
|
||||
<Label label={resolved ? documents.string.Resolved : documents.string.Pending} />
|
||||
</div>
|
||||
</span>
|
||||
{#if $canAddDocumentCommentsFeedback}
|
||||
<div class="tools">
|
||||
<Button
|
||||
|
@ -40,6 +40,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const query = createQuery()
|
||||
const isSecureContext = window.isSecureContext
|
||||
|
||||
interface InviteParams {
|
||||
expirationTime: number
|
||||
@ -87,6 +88,7 @@
|
||||
}
|
||||
}
|
||||
function copy (): void {
|
||||
if (!isSecureContext) return
|
||||
if (link === undefined) return
|
||||
copyTextToClipboard(link)
|
||||
copied = true
|
||||
@ -109,7 +111,7 @@
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div class="antiPopup popup">
|
||||
<div class="antiPopup popup" class:secure={isSecureContext}>
|
||||
<div class="flex-between fs-title mb-9">
|
||||
<Label label={login.string.InviteDescription} />
|
||||
<InviteWorkspace size={'large'} />
|
||||
@ -154,7 +156,9 @@
|
||||
<Loading />
|
||||
{:else if link !== undefined}
|
||||
<!-- 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">
|
||||
<Button
|
||||
label={login.string.Close}
|
||||
@ -164,7 +168,9 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
/>
|
||||
<Button label={copied ? login.string.Copied : login.string.Copy} size={'medium'} on:click={copy} />
|
||||
{#if isSecureContext}
|
||||
<Button label={copied ? login.string.Copied : login.string.Copy} size={'medium'} on:click={copy} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="buttons">
|
||||
@ -195,6 +201,10 @@
|
||||
.link {
|
||||
margin: 1.75rem 0 0;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&.notSecure {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
|
@ -7,6 +7,7 @@
|
||||
import { getProviders } from '../utils'
|
||||
import Github from './providers/Github.svelte'
|
||||
import Google from './providers/Google.svelte'
|
||||
import OpenId from './providers/OpenId.svelte'
|
||||
|
||||
interface Provider {
|
||||
name: string
|
||||
@ -21,6 +22,10 @@
|
||||
{
|
||||
name: '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 { parseLinkId } from '@hcengineering/view-resources'
|
||||
import { get } from 'svelte/store'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
||||
import notification from '../../plugin'
|
||||
@ -179,6 +180,12 @@
|
||||
}
|
||||
|
||||
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) {
|
||||
selectedMessage = get(inboxClient.activityInboxNotifications).find(
|
||||
|
@ -588,6 +588,7 @@ export async function selectInboxContext (
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const { objectId, objectClass } = context
|
||||
const loc = getCurrentLocation()
|
||||
|
||||
if (isMentionNotification(notification) && isActivityMessageClass(notification.mentionedInClass)) {
|
||||
const selectedMsg = notification.mentionedIn as Ref<ActivityMessage>
|
||||
@ -619,17 +620,25 @@ export async function selectInboxContext (
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const selectedMsg = (notification as ActivityInboxNotification)?.attachedTo
|
||||
const thread = selectedMsg !== objectId ? objectId : loc.path[4] === objectId ? objectId : undefined
|
||||
|
||||
void navigateToInboxDoc(
|
||||
linkProviders,
|
||||
objectId,
|
||||
objectClass,
|
||||
selectedMsg !== undefined ? (objectId as Ref<ActivityMessage>) : undefined,
|
||||
thread as Ref<ActivityMessage>,
|
||||
selectedMsg ?? (objectId as Ref<ActivityMessage>)
|
||||
)
|
||||
return
|
||||
|
@ -81,7 +81,7 @@
|
||||
}
|
||||
</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}
|
||||
<ToDoDraggable {todo} {index} {groupName} {projectId} on:drop={handleDrop}>
|
||||
<ToDoElement {todo} planned={mode !== 'unplanned'} />
|
||||
|
@ -237,9 +237,9 @@
|
||||
active: WithLookup<ToDo>[]
|
||||
): [IntlString, WithLookup<ToDo>[]][] {
|
||||
const groups = new Map<IntlString, WithLookup<ToDo>[]>([
|
||||
[time.string.Scheduled, []],
|
||||
[time.string.Unplanned, unplanned],
|
||||
[time.string.ToDos, []],
|
||||
[time.string.Scheduled, []],
|
||||
[time.string.Done, done]
|
||||
])
|
||||
const now = Date.now()
|
||||
|
@ -135,7 +135,8 @@ import {
|
||||
canDeleteSpace,
|
||||
canEditSpace,
|
||||
canJoinSpace,
|
||||
canLeaveSpace
|
||||
canLeaveSpace,
|
||||
isClipboardAvailable
|
||||
} from './visibilityTester'
|
||||
export { canArchiveSpace, canDeleteObject, canDeleteSpace, canEditSpace } from './visibilityTester'
|
||||
export { getActions, getContextActions, invokeAction, showMenu } from './actions'
|
||||
@ -334,6 +335,7 @@ export default async (): Promise<Resources> => ({
|
||||
CanDeleteSpace: canDeleteSpace,
|
||||
CanJoinSpace: canJoinSpace,
|
||||
CanLeaveSpace: canLeaveSpace,
|
||||
IsClipboardAvailable: isClipboardAvailable,
|
||||
BlobImageMetadata: blobImageMetadata,
|
||||
BlobVideoMetadata: blobVideoMetadata
|
||||
}
|
||||
|
@ -143,3 +143,7 @@ export async function canLeaveSpace (doc?: Doc | Doc[]): Promise<boolean> {
|
||||
|
||||
return space.members?.includes(getCurrentAccount()._id)
|
||||
}
|
||||
|
||||
export function isClipboardAvailable (doc?: Doc | Doc[]): boolean {
|
||||
return isSecureContext && navigator.clipboard !== undefined
|
||||
}
|
||||
|
@ -100,6 +100,7 @@
|
||||
hideActions={false}
|
||||
hideDescription={true}
|
||||
adaptive="disabled"
|
||||
closeOnEscape={false}
|
||||
on:close={() => {
|
||||
if (widget !== undefined) {
|
||||
closeWidget(widget._id)
|
||||
@ -168,5 +169,6 @@
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
</style>
|
||||
|
@ -101,7 +101,12 @@ export function openWidget (widget: Widget, data?: Record<string, any>, active =
|
||||
const { widgetsState } = state
|
||||
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({
|
||||
...state,
|
||||
|
@ -52,6 +52,7 @@
|
||||
"passport-custom": "~1.1.1",
|
||||
"passport-google-oauth20": "~2.0.0",
|
||||
"passport-github2": "~0.1.12",
|
||||
"openid-client": "~5.7.0",
|
||||
"koa-passport": "^6.0.0",
|
||||
"koa": "^2.15.3",
|
||||
"koa-router": "^12.0.1",
|
||||
|
@ -12,7 +12,7 @@ export function registerGithub (
|
||||
passport: Passport,
|
||||
router: Router<any, any>,
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
dbPromise: Promise<Db>,
|
||||
frontUrl: string,
|
||||
brandings: BrandingMap
|
||||
): string | undefined {
|
||||
@ -69,6 +69,7 @@ export function registerGithub (
|
||||
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, {
|
||||
githubId: ctx.state.user.id
|
||||
|
@ -12,7 +12,7 @@ export function registerGoogle (
|
||||
passport: Passport,
|
||||
router: Router<any, any>,
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
dbPromise: Promise<Db>,
|
||||
frontUrl: string,
|
||||
brandings: BrandingMap
|
||||
): string | undefined {
|
||||
@ -74,6 +74,7 @@ export function registerGoogle (
|
||||
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)
|
||||
} else {
|
||||
|
@ -5,6 +5,7 @@ import session from 'koa-session'
|
||||
import { Db } from 'mongodb'
|
||||
import { registerGithub } from './github'
|
||||
import { registerGoogle } from './google'
|
||||
import { registerOpenid } from './openid'
|
||||
import { registerToken } from './token'
|
||||
import { BrandingMap, MeasureContext } from '@hcengineering/core'
|
||||
|
||||
@ -15,7 +16,7 @@ export type AuthProvider = (
|
||||
passport: Passport,
|
||||
router: Router<any, any>,
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
db: Promise<Db>,
|
||||
frontUrl: string,
|
||||
brandings: BrandingMap
|
||||
) => string | undefined
|
||||
@ -24,7 +25,7 @@ export function registerProviders (
|
||||
ctx: MeasureContext,
|
||||
app: Koa<Koa.DefaultState, Koa.DefaultContext>,
|
||||
router: Router<any, any>,
|
||||
db: Db,
|
||||
db: Promise<Db>,
|
||||
serverSecret: string,
|
||||
frontUrl: string | undefined,
|
||||
brandings: BrandingMap
|
||||
@ -60,7 +61,7 @@ export function registerProviders (
|
||||
registerToken(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
|
||||
|
||||
const res: string[] = []
|
||||
const providers: AuthProvider[] = [registerGoogle, registerGithub]
|
||||
const providers: AuthProvider[] = [registerGoogle, registerGithub, registerOpenid]
|
||||
for (const provider of providers) {
|
||||
const value = provider(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
|
||||
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,
|
||||
router: Router<any, any>,
|
||||
accountsUrl: string,
|
||||
db: Db,
|
||||
dbPromise: Promise<Db>,
|
||||
frontUrl: string,
|
||||
brandings: BrandingMap
|
||||
): string | undefined {
|
||||
@ -21,9 +21,11 @@ export function registerToken (
|
||||
new CustomStrategy(function (req: any, done: any) {
|
||||
const token = req.body.token ?? req.query.token
|
||||
|
||||
getAccountInfoByToken(measureCtx, db, null, token)
|
||||
.then((user: any) => done(null, user))
|
||||
.catch((err: any) => done(err))
|
||||
void dbPromise.then((db) => {
|
||||
getAccountInfoByToken(measureCtx, db, null, token)
|
||||
.then((user: any) => done(null, user))
|
||||
.catch((err: any) => done(err))
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -76,15 +76,15 @@ export class DocumentCommentsPage extends DocumentCommonPage {
|
||||
reply: string
|
||||
): Promise<void> {
|
||||
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=../../../..')
|
||||
// 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
|
||||
await comment.locator('div.header div.tools button').hover()
|
||||
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
|
||||
// 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
|
||||
await expect(
|
||||
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
|
||||
): Promise<void> {
|
||||
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=../../../..')
|
||||
// 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
|
||||
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()
|
||||
// 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
|
||||
await expect(
|
||||
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())
|
||||
|
||||
void client.getClient().then(async (p: MongoClient) => {
|
||||
const db = p.db(ACCOUNT_DB)
|
||||
registerProviders(measureCtx, app, router, db, serverSecret, frontURL, brandings)
|
||||
const mongoClientPromise = client.getClient()
|
||||
const dbPromise = mongoClientPromise.then((c) => c.db(ACCOUNT_DB))
|
||||
registerProviders(measureCtx, app, router, dbPromise, serverSecret, frontURL, brandings)
|
||||
|
||||
void dbPromise.then((db) => {
|
||||
setInterval(
|
||||
() => {
|
||||
void cleanExpiredOtp(db)
|
||||
|
@ -36,7 +36,7 @@ test.describe('Content in the Documents tests', () => {
|
||||
let documentsSecondPage: DocumentsPage
|
||||
let documentContentSecondPage: DocumentContentPage
|
||||
|
||||
test.beforeEach(async ({ browser, page, request }) => {
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
leftSideMenuPage = new LeftSideMenuPage(page)
|
||||
documentsPage = new DocumentsPage(page)
|
||||
documentContentPage = new DocumentContentPage(page)
|
||||
@ -116,8 +116,8 @@ test.describe('Content in the Documents tests', () => {
|
||||
|
||||
await documentContentPage.inputContentParapraph().click()
|
||||
await documentContentPage.leftMenu().click()
|
||||
await documentContentPage.menuPopupItemButton('Table').click()
|
||||
await documentContentPage.menuPopupItemButton('1x2').first().click()
|
||||
await documentContentPage.clickPopupItem('Table')
|
||||
await documentContentPage.clickPopupItem('1x2')
|
||||
await documentContentPage.proseTableCell(0, 0).fill('One')
|
||||
await documentContentPage.proseTableCell(0, 1).fill('Two')
|
||||
await documentContentPage.buttonInsertColumn().click()
|
||||
@ -295,4 +295,64 @@ test.describe('Content in the Documents tests', () => {
|
||||
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 linkDocument: NewDocument = {
|
||||
title: `Links Document Title-${generateId()}`,
|
||||
|
@ -206,6 +206,10 @@ export class CommonPage {
|
||||
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> {
|
||||
await this.buttonFilter().click()
|
||||
await this.selectPopupMenu(filter).click()
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { type Locator, type Page, expect } from '@playwright/test'
|
||||
import { CommonPage } from '../common-page'
|
||||
import { uploadFile } from '../../utils'
|
||||
import path from 'path'
|
||||
|
||||
export class DocumentContentPage extends CommonPage {
|
||||
readonly page: Page
|
||||
@ -35,7 +36,9 @@ export class DocumentContentPage extends CommonPage {
|
||||
readonly buttonInsertInnerRow = (row: number = 0): Locator =>
|
||||
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 buttonFormLinkSave = (): Locator =>
|
||||
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 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> {
|
||||
await expect(this.buttonDocumentTitle()).toHaveValue(title)
|
||||
}
|
||||
@ -69,10 +84,10 @@ export class DocumentContentPage extends CommonPage {
|
||||
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()).toHaveJSProperty('contentEditable', 'true')
|
||||
await this.inputContent().pressSequentially(`\n${newContent}`)
|
||||
await this.inputContent().pressSequentially(`${newLine ? '\n' : ''}${newContent}`)
|
||||
const endContent = await this.inputContent().textContent()
|
||||
return endContent ?? ''
|
||||
}
|
||||
@ -202,14 +217,70 @@ export class DocumentContentPage extends CommonPage {
|
||||
|
||||
async addLinkToText (text: string, link: string): Promise<void> {
|
||||
await expect(this.page.locator('p', { hasText: text })).toBeVisible()
|
||||
await this.page.locator('p', { hasText: text }).click()
|
||||
await this.page.locator('p', { hasText: text }).dblclick()
|
||||
await this.buttonToolbarLink().click()
|
||||
await this.page.locator('p', { hasText: text }).click({ clickCount: 3 })
|
||||
await this.buttonOnToolbar('btnLink').click()
|
||||
|
||||
await this.inputFormLink().fill(link)
|
||||
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> {
|
||||
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.selectMenuItem(this.page, talentName.finalContactName)
|
||||
|
||||
await expect(
|
||||
this.buttonMergeRow().locator('div.flex-center', { hasText: talentName.name }).locator('label.checkbox-container')
|
||||
).toBeVisible()
|
||||
|
||||
await this.buttonMergeRow()
|
||||
.locator('div.flex-center', { hasText: talentName.name })
|
||||
.locator('label.checkbox-container')
|
||||
|
Loading…
Reference in New Issue
Block a user