Merge branch 'develop' into staging-new

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-02-19 14:13:04 +07:00
84 changed files with 768 additions and 427 deletions
+3 -5
View File
@@ -134,12 +134,10 @@ Before you can begin, you need to create a workspace and an account and associat
```bash
cd ./tool # dev/tool in the repository root
rushx run-local create-workspace ws1 -w DevWorkspace # Create workspace
rushx run-local create-account user1 -p 1234 -f John -l Appleseed # Create account
rushx run-local configure ws1 --list --enable '*' # Enable all modules, even if they are not yet intended to be used by a wide audience.
rushx run-local assign-workspace user1 ws1 # Assign workspace to user.
rushx run-local confirm-email user1 # To allow the creation of additional test workspaces.
rushx run-local create-workspace ws1 email:user1 # Create workspace
rushx run-local configure ws1 --list --enable '*' # Enable all modules, even if they are not yet intended to be used by a wide audience
rushx run-local assign-workspace user1 ws1 # Assign user to workspace
```
Alternatively, you can just execute:
+1 -1
View File
@@ -21,7 +21,7 @@
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool",
"run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4700 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local-cr": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local-cr": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js",
"run-local-brk": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --inspect-brk --enable-source-maps --max-old-space-size=18000 ./bundle/bundle.js",
"run": "rush bundle --to @hcengineering/tool >/dev/null && cross-env node --max-old-space-size=8000 ./bundle/bundle.js",
"upgrade": "rushx run-local upgrade",
+11 -3
View File
@@ -18,10 +18,10 @@ import core, { IndexKind } from '@hcengineering/core'
import { type Builder, Index, Model, Prop, TypeString, UX } from '@hcengineering/model'
import contact from '@hcengineering/model-contact'
import view from '@hcengineering/model-view'
import { TChunterSpace } from '@hcengineering/model-chunter'
import { TChunterSpace, TChatMessage } from '@hcengineering/model-chunter'
import chunter from '@hcengineering/chunter'
import mail, { type MailThread } from '@hcengineering/mail'
import mail, { type MailThread, type MailMessage } from '@hcengineering/mail'
export { mailId } from '@hcengineering/mail'
export { default } from './plugin'
@@ -49,8 +49,16 @@ export class TMailThread extends TChunterSpace implements MailThread {
preview!: string
}
@Model(mail.class.MailMessage, chunter.class.ChatMessage)
@UX(mail.string.MailMessage, undefined, undefined, undefined, undefined, mail.string.MailMessages)
export class TMailMessage extends TChatMessage implements MailMessage {
@Prop(TypeString(), mail.string.MailId)
@Index(IndexKind.Indexed)
mailId!: string
}
export function createModel (builder: Builder): void {
builder.createModel(TMailThread)
builder.createModel(TMailThread, TMailMessage)
builder.mixin(mail.class.MailThread, core.class.Class, activity.mixin.ActivityDoc, {})
+3
View File
@@ -52,6 +52,9 @@ export function createModel (builder: Builder): void {
label: mySpace.string.Mail,
createLabel: mail.string.CreateMail,
createComponent: mail.component.CreateMail
},
queryOptions: {
filterBySpace: true
}
}
]
+9
View File
@@ -32,6 +32,15 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCard.trigger.OnMasterTagRemove,
isAsync: true,
txMatch: {
_class: core.class.TxRemoveDoc,
objectClass: card.class.MasterTag
}
})
builder.mixin(card.class.Card, core.class.Class, serverCore.mixin.SearchPresenter, {
searchIcon: card.icon.Card,
title: [['title']]
+16 -6
View File
@@ -16,7 +16,7 @@
import activity from '@hcengineering/activity'
import notification, { type NotificationType } from '@hcengineering/notification'
import { type Asset, type IntlString } from '@hcengineering/platform'
import type { BuildModelKey, Viewlet, ViewletDescriptor } from '@hcengineering/view'
import type { BuildModelKey, KeyFilterPreset, Viewlet, ViewletDescriptor } from '@hcengineering/view'
import questions from '@hcengineering/model-questions'
import contact from '@hcengineering/contact'
import tracker from '@hcengineering/model-tracker'
@@ -659,9 +659,19 @@ function defineTrainingAttempt (builder: Builder): void {
sortingKey: 'state',
displayProps: { align: 'center' }
}
const columnOwner: BuildModelKey = {
...columns.owner,
key: 'owner'
const columnTrainee: BuildModelKey = {
key: 'owner',
label: training.string.TrainingRequestTrainee,
presenter: contacts.component.EmployeePresenter,
props: { shouldShowName: true },
displayProps: { align: 'center' }
}
const columnTraineeFilter: KeyFilterPreset = {
_class: training.class.TrainingAttempt,
component: contacts.component.EmployeeFilter,
key: 'owner',
label: training.string.TrainingRequestTrainee
}
defineTableBrowserViewletDescriptor(
@@ -693,7 +703,7 @@ function defineTrainingAttempt (builder: Builder): void {
columnScore,
'createdOn',
'submittedOn',
columnOwner
columnTrainee
],
configOptions: {
strict: true,
@@ -720,7 +730,7 @@ function defineTrainingAttempt (builder: Builder): void {
})
builder.mixin(training.class.TrainingAttempt, core.class.Class, view.mixin.ClassFilters, {
filters: ['state', 'owner', 'submittedOn'] as Array<keyof TrainingAttempt>,
filters: ['state', 'submittedOn', columnTraineeFilter] as Array<keyof TrainingAttempt>,
strict: true
})
+12 -2
View File
@@ -48,7 +48,8 @@ export interface AccountClient {
restorePassword: (password: string) => Promise<LoginInfo>
confirm: () => Promise<LoginInfo>
requestPasswordReset: (email: string) => Promise<void>
sendInvite: (email: string, role?: AccountRole) => Promise<void>
sendInvite: (email: string, role: AccountRole) => Promise<void>
resendInvite: (email: string, role: AccountRole) => Promise<void>
leaveWorkspace: (account: string) => Promise<LoginInfo | null>
changeUsername: (first: string, last: string) => Promise<void>
changePassword: (oldPassword: string, newPassword: string) => Promise<void>
@@ -255,7 +256,7 @@ class AccountClientImpl implements AccountClient {
await this.rpc(request)
}
async sendInvite (email: string, role?: AccountRole): Promise<void> {
async sendInvite (email: string, role: AccountRole): Promise<void> {
const request = {
method: 'sendInvite' as const,
params: [email, role]
@@ -264,6 +265,15 @@ class AccountClientImpl implements AccountClient {
await this.rpc(request)
}
async resendInvite (email: string, role: AccountRole): Promise<void> {
const request = {
method: 'resendInvite' as const,
params: [email, role]
}
await this.rpc(request)
}
async leaveWorkspace (account: string): Promise<LoginInfo | null> {
const request = {
method: 'leaveWorkspace' as const,
@@ -13,37 +13,43 @@
// limitations under the License.
-->
<script lang="ts">
import { generateId } from '@hcengineering/core'
import { Doc, generateId, Ref } from '@hcengineering/core'
import { ViewContext } from '@hcengineering/view'
import { onDestroy } from 'svelte'
import { ContextStore, contextStore } from '../context'
interface ViewContextWithId extends ViewContext {
id?: Ref<Doc>
}
export let context: ViewContext
const id = generateId()
$: len = $contextStore.contexts.findIndex((it) => (it as any).id === id)
onDestroy(() => {
contextStore.update((t) => {
return new ContextStore(t.contexts.slice(0, len ?? 0))
contextStore.update((cur) => {
const contexts = cur.contexts as ViewContextWithId[]
const pos = contexts.findIndex((it) => it.id === id)
if (pos === -1) {
return cur
}
return new ContextStore(contexts.slice(0, pos))
})
})
$: {
contextStore.update((cur) => {
const pos = cur.contexts.findIndex((it) => (it as any).id === id)
const newCur = {
const contexts = cur.contexts as ViewContextWithId[]
const pos = contexts.findIndex((it) => it.id === id)
const newCur: ViewContextWithId = {
id,
mode: context.mode,
application: context.application ?? cur.contexts[(pos !== -1 ? pos : cur.contexts.length) - 1]?.application
application: context.application ?? contexts[(pos !== -1 ? pos : contexts.length) - 1]?.application
}
if (pos === -1) {
len = cur.contexts.length
return new ContextStore([...cur.contexts, newCur])
return new ContextStore([...contexts, newCur])
}
len = pos
return new ContextStore([...cur.contexts.slice(0, pos), newCur])
return new ContextStore(contexts.map((it) => (it.id === id ? newCur : it)))
})
}
</script>
+1 -1
View File
@@ -56,7 +56,7 @@ export const DefaultKit = Extension.create<DefaultKitOptions>({
}),
Typography.configure({}),
Link.extend({ inclusive: false }).configure({
openOnClick: true,
openOnClick: false,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
})
]
+3 -3
View File
@@ -68,9 +68,9 @@
--text-editor-selected-node-background: rgba(43, 81, 144, 0.1);
--text-editor-selected-node-color: #93CAF3;
--text-editor-highlighted-node-warning-active-background-color: #F2D7AE;
--text-editor-highlighted-node-warning-background-color: #F8EBD7;
--text-editor-highlighted-node-warning-border-color: #DE9B35;
--text-editor-highlighted-node-warning-active-background-color: rgba(255, 203, 0, .24);
--text-editor-highlighted-node-warning-background-color: rgba(255, 203, 0, .12);
--text-editor-highlighted-node-warning-border-color: rgba(255, 203, 0, .35);
--text-editor-highlighted-node-add-background-color: #DAEDDC;
--text-editor-highlighted-node-add-font-color: #1C4220;
+3 -1
View File
@@ -323,7 +323,9 @@
.text-editor-highlighted-node-warning {
background-color: var(--text-editor-highlighted-node-warning-background-color);
border-bottom: 0.0625rem solid var(--text-editor-highlighted-node-warning-border-color);
border-bottom: 2px solid var(--text-editor-highlighted-node-warning-border-color);
padding-bottom: 2px;
transition: background 0.2s ease, border 0.2s ease;
&.text-editor-highlighted-node-selected, &:hover {
background-color: var(--text-editor-highlighted-node-warning-active-background-color);
@@ -16,21 +16,20 @@
-->
<script lang="ts">
import card, { Card, Tag } from '@hcengineering/card'
import { Classifier, fillDefaults, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
ButtonIcon,
CircleButton,
eventToHTMLElement,
IconAdd,
IconClose,
Label,
showPopup,
ScrollerBar,
SelectPopup,
SelectPopupValueType,
eventToHTMLElement,
ScrollerBar
showPopup
} from '@hcengineering/ui'
import MasterTagSelector from './MasterTagSelector.svelte'
import { fillDefaults } from '@hcengineering/core'
export let doc: Card
@@ -50,11 +49,23 @@
}
$: ancestors = hierarchy.getAncestors(doc._class)
$: possibleMixins = tags.filter(
(p) =>
!hierarchy.hasMixin(doc, p._id) &&
(hierarchy.isDerived(p._id, doc._class) || ancestors.includes(hierarchy.getBaseClass(p._id)))
)
$: possibleMixins = getPossibleMixins(ancestors, tags)
function getPossibleMixins (ancestors: Ref<Classifier>[], tags: Tag[]): Tag[] {
const res: Tag[] = []
for (const p of tags) {
try {
if (hierarchy.hasMixin(doc, p._id)) continue
const base = hierarchy.getBaseClass(p._id)
if (hierarchy.isDerived(p._id, doc._class) || ancestors.includes(base)) {
res.push(p)
}
} catch (err) {
console.log('error', err, p._id)
}
}
return res
}
$: dropdownItems = possibleMixins.map((mixin) => ({ id: mixin._id, label: mixin.label }))
function add (e: MouseEvent): void {
showPopup(
@@ -16,7 +16,15 @@
import { MasterTag } from '@hcengineering/card'
import { getEmbeddedLabel, translateCB } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { ButtonIcon, IconDelete, ModernEditbox, showPopup, themeStore } from '@hcengineering/ui'
import {
ButtonIcon,
getCurrentLocation,
IconDelete,
ModernEditbox,
navigate,
showPopup,
themeStore
} from '@hcengineering/ui'
import { IconPicker } from '@hcengineering/view-resources'
import card from '../../plugin'
import { deleteMasterTag } from '../../utils'
@@ -40,7 +48,15 @@
}
async function handleDelete (): Promise<void> {
await deleteMasterTag(masterTag)
await deleteMasterTag(masterTag, () => {
const loc = getCurrentLocation()
if (masterTag.extends !== card.class.MasterTag && masterTag.extends !== undefined) {
loc.path[4] = masterTag.extends
} else {
loc.path.length = 3
}
navigate(loc)
})
}
function setIcon (): void {
@@ -15,7 +15,7 @@
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
BreadcrumbItem,
Breadcrumbs,
@@ -45,12 +45,10 @@
const client = getClient()
const hierarchy = client.getHierarchy()
$: masterTag = getMasterTag(selectedTagId)
function getMasterTag (_id: Ref<MasterTag> | undefined): MasterTag | undefined {
if (_id === undefined) return undefined
return client.getModel().findObject(_id)
}
const query = createQuery()
$: query.query(card.class.MasterTag, { _id: selectedTagId }, (res) => {
masterTag = res[0]
})
function getBreadcrumbs (tag: Ref<MasterTag> | undefined): BreadcrumbItem[] {
if (tag === undefined) return []
+6 -43
View File
@@ -36,30 +36,17 @@ import { type LocationData } from '@hcengineering/workbench'
import CardSearchItem from './components/CardSearchItem.svelte'
import card from './plugin'
export async function deleteMasterTag (tag: MasterTag | undefined): Promise<void> {
export async function deleteMasterTag (tag: MasterTag | undefined, onDelete?: () => void): Promise<void> {
if (tag !== undefined) {
const client = getClient()
const objects = await client.findAll(tag._id, {})
if (objects.length > 0) {
const objects = await client.findOne(tag._id, {})
if (objects !== undefined) {
if (tag._class === card.class.MasterTag) {
showPopup(MessageBox, {
label: card.string.DeleteMasterTag,
message: card.string.DeleteMasterTagConfirm,
action: async () => {
const cards = await client.findAll(tag._id, {})
const hierarchy = client.getHierarchy()
const ops = client.apply(undefined, 'delete-master-tag')
for (const obj of cards) {
await ops.remove(obj)
}
const desc = hierarchy.getDescendants(tag._id)
for (const obj of desc) {
if (obj === tag._id) continue
if (!hierarchy.isMixin(obj)) continue
const desc = hierarchy.getClass(obj)
await ops.remove(desc)
}
await ops.commit()
onDelete?.()
await client.remove(tag)
}
})
@@ -68,37 +55,13 @@ export async function deleteMasterTag (tag: MasterTag | undefined): Promise<void
label: card.string.DeleteTag,
message: card.string.DeleteTagConfirm,
action: async () => {
const cards = await client.findAll(tag._id, {})
const ops = client.apply(undefined, 'delete-tag')
const hierarchy = client.getHierarchy()
const desc = hierarchy.getDescendants(tag._id)
for (const obj of desc) {
if (obj === tag._id) continue
const desc = hierarchy.getClass(obj)
await ops.remove(desc)
}
const update: Record<string, boolean> = {}
for (const des of desc) {
update[des] = true
}
for (const obj of cards) {
await ops.update(obj, { $unset: update })
}
await ops.commit()
onDelete?.()
await client.remove(tag)
}
})
}
} else {
const ops = client.apply(undefined, 'delete-tag')
const hierarchy = client.getHierarchy()
const desc = hierarchy.getDescendants(tag._id)
for (const obj of desc) {
if (obj === tag._id) continue
const desc = hierarchy.getClass(obj)
await ops.remove(desc)
}
await ops.commit()
onDelete?.()
await client.remove(tag)
}
}
+13 -7
View File
@@ -23,6 +23,8 @@ import {
type Person
} from '@hcengineering/contact'
import {
AccountRole,
SocialIdType,
type Class,
type Client,
type Data,
@@ -263,18 +265,22 @@ async function doContactQuery<T extends Contact> (
}
async function resendInvite (doc: Person): Promise<void> {
// const client = getClient()
const client = getClient()
const emailSocialId = await client.findOne(contact.class.SocialIdentity, {
attachedTo: doc._id,
type: SocialIdType.EMAIL
})
if (emailSocialId == null) {
console.error('Cannot find email social id for person', doc._id)
return
}
showPopup(MessageBox, {
label: contact.string.ResendInvite,
message: contact.string.ResendInviteDescr,
action: async () => {
// TODO: FIXME
throw new Error('Not implemented')
// const _resendInvite = await getResource(login.function.ResendInvite)
// for (const i of accounts) {
// await _resendInvite(i.email)
// }
const _resendInvite = await getResource(login.function.ResendInvite)
await _resendInvite(emailSocialId?.value, AccountRole.User)
}
})
}
+5 -1
View File
@@ -439,7 +439,11 @@ export async function ensureEmployee (
if (me.role !== AccountRole.Guest) {
const employee = await client.findOne(contact.mixin.Employee, { _id: personRef as Ref<Employee> })
if (employee === undefined || !client.getHierarchy().hasMixin(employee, contact.mixin.Employee)) {
if (
employee === undefined ||
!client.getHierarchy().hasMixin(employee, contact.mixin.Employee) ||
!employee.active
) {
await ctx.with('create-employee', {}, async () => {
if (personRef === undefined) {
// something went wrong
@@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="checkmark-circle" viewBox="0 0 20 20">
<path d="M10 1.25C8.26942 1.25 6.57769 1.76318 5.13876 2.72464C3.69983 3.6861 2.57832 5.05267 1.91606 6.65152C1.25379 8.25037 1.08051 10.0097 1.41813 11.707C1.75575 13.4044 2.58911 14.9635 3.81282 16.1872C5.03653 17.4109 6.59563 18.2442 8.29296 18.5819C9.9903 18.9195 11.7496 18.7462 13.3485 18.0839C14.9473 17.4217 16.3139 16.3002 17.2754 14.8612C18.2368 13.4223 18.75 11.7306 18.75 10C18.75 7.67936 17.8281 5.45376 16.1872 3.81282C14.5462 2.17187 12.3206 1.25 10 1.25ZM10 17.5C8.51664 17.5 7.0666 17.0601 5.83323 16.236C4.59986 15.4119 3.63856 14.2406 3.07091 12.8701C2.50325 11.4997 2.35473 9.99168 2.64411 8.53682C2.9335 7.08197 3.64781 5.74559 4.6967 4.6967C5.7456 3.64781 7.08197 2.9335 8.53683 2.64411C9.99168 2.35472 11.4997 2.50325 12.8701 3.0709C14.2406 3.63856 15.4119 4.59985 16.236 5.83322C17.0601 7.06659 17.5 8.51664 17.5 10C17.5 11.9891 16.7098 13.8968 15.3033 15.3033C13.8968 16.7098 11.9891 17.5 10 17.5Z" fill="black" fill-opacity="0.8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1919 7.05821C14.436 7.30229 14.436 7.69802 14.1919 7.94209L9.19194 12.9421C8.94786 13.1862 8.55214 13.1862 8.30806 12.9421L5.80806 10.4421C5.56398 10.198 5.56398 9.80229 5.80806 9.55821C6.05214 9.31413 6.44786 9.31413 6.69194 9.55821L8.75 11.6163L13.3081 7.05821C13.5521 6.81413 13.9479 6.81413 14.1919 7.05821Z" fill="black" fill-opacity="0.8"/>
<path d="M10 1.25C8.26942 1.25 6.57769 1.76318 5.13876 2.72464C3.69983 3.6861 2.57832 5.05267 1.91606 6.65152C1.25379 8.25037 1.08051 10.0097 1.41813 11.707C1.75575 13.4044 2.58911 14.9635 3.81282 16.1872C5.03653 17.4109 6.59563 18.2442 8.29296 18.5819C9.9903 18.9195 11.7496 18.7462 13.3485 18.0839C14.9473 17.4217 16.3139 16.3002 17.2754 14.8612C18.2368 13.4223 18.75 11.7306 18.75 10C18.75 7.67936 17.8281 5.45376 16.1872 3.81282C14.5462 2.17187 12.3206 1.25 10 1.25ZM10 17.5C8.51664 17.5 7.0666 17.0601 5.83323 16.236C4.59986 15.4119 3.63856 14.2406 3.07091 12.8701C2.50325 11.4997 2.35473 9.99168 2.64411 8.53682C2.9335 7.08197 3.64781 5.74559 4.6967 4.6967C5.7456 3.64781 7.08197 2.9335 8.53683 2.64411C9.99168 2.35472 11.4997 2.50325 12.8701 3.0709C14.2406 3.63856 15.4119 4.59985 16.236 5.83322C17.0601 7.06659 17.5 8.51664 17.5 10C17.5 11.9891 16.7098 13.8968 15.3033 15.3033C13.8968 16.7098 11.9891 17.5 10 17.5Z" fill="currentColor" fill-opacity="0.8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1919 7.05821C14.436 7.30229 14.436 7.69802 14.1919 7.94209L9.19194 12.9421C8.94786 13.1862 8.55214 13.1862 8.30806 12.9421L5.80806 10.4421C5.56398 10.198 5.56398 9.80229 5.80806 9.55821C6.05214 9.31413 6.44786 9.31413 6.69194 9.55821L8.75 11.6163L13.3081 7.05821C13.5521 6.81413 13.9479 6.81413 14.1919 7.05821Z" fill="currentColor" fill-opacity="0.8"/>
</symbol>
<symbol id="documentapplication" viewBox="0 0 32 32">
<path d="M10 14C9.44772 14 9 14.4477 9 15C9 15.5523 9.44772 16 10 16H22C22.5523 16 23 15.5523 23 15C23 14.4477 22.5523 14 22 14H10Z"/>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@@ -20,7 +20,7 @@
import { getClient } from '@hcengineering/presentation'
import view from '@hcengineering/view'
import attachment, { Attachment } from '@hcengineering/attachment'
import documents from '@hcengineering/controlled-documents'
import documents, { DocumentState } from '@hcengineering/controlled-documents'
import { Editor, Heading } from '@hcengineering/text-editor'
import {
CollaboratorEditor,
@@ -34,8 +34,9 @@
highlightUpdateCommand,
getNodeElement
} from '@hcengineering/text-editor-resources'
import { navigate, EditBox, Scroller } from '@hcengineering/ui'
import { navigate, EditBox, Scroller, Label } from '@hcengineering/ui'
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
import plugin from '../../plugin'
import {
$areDocumentCommentPopupsOpened as areDocumentCommentPopupsOpened,
@@ -248,7 +249,7 @@
<TableOfContents items={headings} enumerated={true} on:select={(ev) => handleShowHeading(ev.detail)} />
</div>
<Scroller>
<div class="content">
<div class="content relative">
<DocumentTitle>
{#if $isEditable}
<EditBox
@@ -262,6 +263,13 @@
{$controlledDocument.title}
{/if}
</DocumentTitle>
{#if $controlledDocument.state === DocumentState.Obsolete}
<div class="watermark-container">
{#each { length: 24 } as _, i}
<div class="watermark"><Label label={plugin.string.Obsolete} /></div>
{/each}
</div>
{/if}
<CollaboratorEditor
bind:this={textEditor}
object={$controlledDocument}
@@ -351,6 +359,34 @@
}
.bottomSpacing {
padding-bottom: 30vh;
padding-bottom: 55vh;
}
.watermark-container {
position: absolute;
z-index: 100;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 35rem;
padding-top: 20rem;
overflow: hidden;
pointer-events: none;
}
.watermark {
z-index: 100;
margin: auto;
height: 4rem;
width: 100%;
color: var(--theme-divider-color);
font-size: 8rem;
transform: rotate(-45deg);
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -58,33 +58,33 @@
const client = getClient()
async function changePlannedEffectiveDate (plannedEffectiveDate: Timestamp) {
if (!$controlledDocument) {
return
}
// async function changePlannedEffectiveDate (plannedEffectiveDate: Timestamp) {
// if (!$controlledDocument) {
// return
// }
await client.update($controlledDocument, { plannedEffectiveDate })
}
// await client.update($controlledDocument, { plannedEffectiveDate })
// }
let selectedDate: Timestamp =
$controlledDocument?.plannedEffectiveDate != null && $controlledDocument?.plannedEffectiveDate > 0
? $controlledDocument.plannedEffectiveDate
: Date.now()
// let selectedDate: Timestamp =
// $controlledDocument?.plannedEffectiveDate != null && $controlledDocument?.plannedEffectiveDate > 0
// ? $controlledDocument.plannedEffectiveDate
// : Date.now()
let selected: IntlString | undefined = undefined
if ($controlledDocument?.plannedEffectiveDate === 0) {
selected = documentsRes.string.EffectiveImmediately
} else if ($controlledDocument?.plannedEffectiveDate != null) {
selected = documentsRes.string.EffectiveOn
}
// let selected: IntlString | undefined = undefined
// if ($controlledDocument?.plannedEffectiveDate === 0) {
// selected = documentsRes.string.EffectiveImmediately
// } else if ($controlledDocument?.plannedEffectiveDate != null) {
// selected = documentsRes.string.EffectiveOn
// }
async function changeSelectedDate (ev: CustomEvent) {
if (ev.detail !== undefined) {
selectedDate = ev.detail
await changePlannedEffectiveDate(ev.detail)
selected = documentsRes.string.EffectiveOn
}
}
// async function changeSelectedDate (ev: CustomEvent) {
// if (ev.detail !== undefined) {
// selectedDate = ev.detail
// await changePlannedEffectiveDate(ev.detail)
// selected = documentsRes.string.EffectiveOn
// }
// }
const reviewIntervals: DropdownTextItem[] = []
for (const interval of periodicReviewIntervals) {
@@ -254,10 +254,10 @@
gap="none"
/>
</div>
<header class="fs-title text-lg my-4">
<header class="fs-title text-lg mt-4">
<Label label={documentsRes.string.EffectiveDocumentLifecycle} />
</header>
<span class="fs-title text-normal">
<!-- <span class="fs-title text-normal">
<Label label={documentsRes.string.EffectiveDate} />
</span>
<div>
@@ -298,7 +298,7 @@
</div>
</RadioButton>
</div>
</div>
</div> -->
<div class="flex-row-center flex-gap-2">
<span class="whitespace-nowrap fs-title text-normal">
<Label label={documentsRes.string.PeriodicReviewToBeCompleted} />
@@ -54,7 +54,7 @@
$controlledDocument != null &&
(isOrgSpace
? $controlledDocument.state !== DocumentState.Effective
: ![DocumentState.Effective, DocumentState.Obsolete, DocumentState.Archived].includes($controlledDocument.state))
: ![DocumentState.Effective, DocumentState.Archived].includes($controlledDocument.state))
</script>
{#if $controlledDocument !== null}
@@ -17,8 +17,7 @@
.theme-dark {
--theme-docs-contrast-color: #ffffff;
--theme-docs-description-border-color: rgba(0, 0, 0, 0.2);
--theme-docs-frozen-description-color: #e7f2f3;
--theme-docs-comment-highlighted-color: #fefbf1;
--theme-docs-comment-highlighted-color: var(--theme-comp-header-color);
--theme-docs-warning-color: rgba(222, 155, 53, 0.08);
--theme-docs-warning-icon-color: #d27540;
--theme-docs-accepted-color: #38833f;
@@ -28,8 +27,7 @@
.theme-light {
--theme-docs-contrast-color: #000000;
--theme-docs-description-border-color: rgba(0, 0, 0, 0.2);
--theme-docs-frozen-description-color: #e7f2f3;
--theme-docs-comment-highlighted-color: #fefbf1;
--theme-docs-comment-highlighted-color: var(--theme-comp-header-color);
--theme-docs-warning-color: rgba(222, 155, 53, 0.08);
--theme-docs-warning-icon-color: #d27540;
--theme-docs-accepted-color: #38833f;
+2
View File
@@ -52,6 +52,8 @@
"CanFindCode": "Nemůžete najít svůj kód? Zkontrolujte složku nevyžádané pošty.",
"LoginWithPassword": "Přihlásit se pomocí hesla",
"LoginWithCode": "Přihlásit se pomocí kódu",
"SignUpWithPassword": "Registrace pomocí hesla",
"SignUpWithCode": "Registrace pomocí kódu",
"FillInProfile": "Vyplňte svůj profil",
"SetUpPassword": "Nastavte si heslo",
"Next": "Další",
+2
View File
@@ -52,6 +52,8 @@
"CanFindCode": "Code nicht gefunden? Überprüfen Sie Ihren Spam-Ordner.",
"LoginWithPassword": "Mit Passwort anmelden",
"LoginWithCode": "Mit Code anmelden",
"SignUpWithPassword": "Mit Passwort registrieren",
"SignUpWithCode": "Mit Code registrieren",
"FillInProfile": "Profil ausfüllen",
"SetUpPassword": "Passwort einrichten",
"Next": "Weiter",
+2
View File
@@ -51,6 +51,8 @@
"CanFindCode": "Can't find your code? Check your spam folder.",
"LoginWithPassword": "Login with password",
"LoginWithCode": "Login with code",
"SignUpWithPassword": "Sign up with password",
"SignUpWithCode": "Sign up with code",
"FillInProfile": "Fill in your profile",
"SetUpPassword": "Set up your password",
"Next": "Next",
+2
View File
@@ -51,6 +51,8 @@
"CanFindCode": "¿No encuentras tu código? Revisa tu",
"LoginWithPassword": "Iniciar sesión con contraseña",
"LoginWithCode": "Iniciar sesión con código",
"SignUpWithPassword": "Registrarse con contraseña",
"SignUpWithCode": "Registrarse con código",
"FillInProfile": "Rellena tu perfil",
"SetUpPassword": "Establecer tu contraseña",
"Next": "Siguiente",
+2
View File
@@ -51,6 +51,8 @@
"CanFindCode": "Vous ne trouvez pas votre code ? Vérifiez votre",
"LoginWithPassword": "Connexion avec mot de passe",
"LoginWithCode": "Connexion avec code",
"SignUpWithPassword": "Inscrivez-vous avec votre mot de passe",
"SignUpWithCode": "Inscrivez-vous avec votre code",
"FillInProfile": "Remplissez votre profil",
"SetUpPassword": "Définir votre mot de passe",
"Next": "Suivant",
+2
View File
@@ -51,6 +51,8 @@
"CanFindCode": "Non riesci a trovare il tuo codice? Controlla la tua cartella spam.",
"LoginWithPassword": "Accedi con la password",
"LoginWithCode": "Accedi con il codice",
"SignUpWithPassword": "Registrati con la password",
"SignUpWithCode": "Registrati con il codice",
"FillInProfile": "Compila il tuo profilo",
"SetUpPassword": "Imposta la tua password",
"Next": "Avanti",
+2
View File
@@ -51,6 +51,8 @@
"CanFindCode": "Não encontra o seu código? Verifique a sua pasta de spam.",
"LoginWithPassword": "Iniciar sessão com palavra-passe",
"LoginWithCode": "Iniciar sessão com código",
"SignUpWithPassword": "Registar com palavra-passe",
"SignUpWithCode": "Registar com código",
"FillInProfile": "Preencha o seu perfil",
"SetUpPassword": "Definir a sua palavra-passe",
"Next": "Seguinte",
+2
View File
@@ -51,6 +51,8 @@
"CanFindCode": "Не нашли код? Проверьте папку со спамом.",
"LoginWithPassword": "Войти с паролем",
"LoginWithCode": "Войти с кодом",
"SignUpWithPassword": "Регистрация с паролем",
"SignUpWithCode": "Регистрация с кодом",
"FillInProfile": "Заполните профиль",
"SetUpPassword": "Установите пароль",
"Next": "Дальше",
+2
View File
@@ -51,6 +51,8 @@
"CanFindCode": "找不到验证码?请检查您的垃圾邮件文件夹。",
"LoginWithPassword": "使用密码登录",
"LoginWithCode": "使用代码登录",
"SignUpWithPassword": "注册密码",
"SignUpWithCode": "注册代码",
"FillInProfile": "填写您的个人资料",
"SetUpPassword": "设置您的密码",
"Next": "下一个",
@@ -86,6 +86,10 @@
return true
})
export function invalidate (): void {
void validate($themeStore.language)
}
$: if ($themeStore.language != null && $themeStore.language !== '') {
void validate($themeStore.language)
}
@@ -143,7 +143,7 @@
{#if page === 'login'}
<LoginForm {navigateUrl} {signUpDisabled} />
{:else if page === 'signup'}
<SignupForm {signUpDisabled} />
<SignupForm {navigateUrl} {signUpDisabled} />
{:else if page === 'createWorkspace'}
<CreateWorkspaceForm />
{:else if page === 'password'}
@@ -29,6 +29,7 @@
export let email: string
export let retryOn: Timestamp
export let signUpDisabled = false
export let loginState: 'login' | 'signup' | 'none' = 'none'
const dispatch = createEventDispatcher()
@@ -233,7 +234,7 @@
style:min-height={$deviceInfo.docHeight > 720 ? '42rem' : '0'}
>
<div class="header">
<Tabs loginState="login" {signUpDisabled} />
<Tabs {loginState} {signUpDisabled} />
<div class="description">
<Label label={login.string.SentTo} />
<span class="email ml-1">
@@ -17,27 +17,40 @@
import { OK, Severity, Status, setMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { setMetadataLocalStorage } from '@hcengineering/ui'
import BottomActionComponent from './BottomAction.svelte'
import login from '../plugin'
import { getPasswordValidationRules } from '../validations'
import { goTo, signUp } from '../utils'
import Form from './Form.svelte'
import { BottomAction, LoginMethods, OtpLoginSteps, signUpOtp } from '../index'
import type { Field } from '../types'
import OtpForm from './OtpForm.svelte'
export let signUpDisabled = false
export let navigateUrl: string | undefined = undefined
const fields: Array<Field> = [
{ id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true },
{ id: 'family-name', name: 'last', i18n: login.string.LastName, short: true },
{ id: 'email', name: 'username', i18n: login.string.Email },
{
id: 'new-password',
name: 'password',
i18n: login.string.Password,
password: true,
rules: getPasswordValidationRules()
},
{ id: 'new-password', name: 'password2', i18n: login.string.PasswordRepeat, password: true }
]
let method: LoginMethods = LoginMethods.Otp
let fields: Array<Field>
let form: Form
$: {
fields = [
{ id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true },
{ id: 'family-name', name: 'last', i18n: login.string.LastName, short: true },
{ id: 'email', name: 'username', i18n: login.string.Email }
]
if (method === LoginMethods.Password) {
fields.push({
id: 'new-password',
name: 'password',
i18n: login.string.Password,
password: true,
rules: getPasswordValidationRules()
})
fields.push({ id: 'new-password', name: 'password2', i18n: login.string.PasswordRepeat, password: true })
}
}
const object = {
first: '',
@@ -48,6 +61,8 @@
}
let status: Status<any> = OK
let step = OtpLoginSteps.Email
let otpRetryOn = 0
if (signUpDisabled) {
goTo('login')
@@ -58,17 +73,70 @@
func: async () => {
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] = await signUp(object.username, object.password, object.first, object.last)
if (method === LoginMethods.Password) {
const [loginStatus, result] = await signUp(object.username, object.password, object.first, object.last)
status = loginStatus
status = loginStatus
if (result != null) {
setMetadata(presentation.metadata.Token, result.token)
setMetadataLocalStorage(login.metadata.LastToken, result.token)
goTo('confirmationSend')
if (result != null) {
setMetadata(presentation.metadata.Token, result.token)
setMetadataLocalStorage(login.metadata.LastToken, result.token)
goTo('confirmationSend')
}
} else {
const [otpStatus, result] = await signUpOtp(object.username, object.first, object.last)
status = otpStatus
if (result?.sent === true && otpStatus === OK) {
step = OtpLoginSteps.Otp
otpRetryOn = result.retryOn
}
}
}
}
let changeMethodAction: BottomAction
$: changeMethodAction = {
i18n: method === LoginMethods.Password ? login.string.SignUpWithCode : login.string.SignUpWithPassword,
func: () => {
method = method === LoginMethods.Password ? LoginMethods.Otp : LoginMethods.Password
if (method === LoginMethods.Password) {
step = OtpLoginSteps.Email
}
setTimeout(() => {
if (form != null) {
form.invalidate()
}
}, 0)
}
}
function handleStep (event: CustomEvent<OtpLoginSteps>): void {
step = event.detail
}
</script>
<Form caption={login.string.SignUp} {status} {fields} {object} {action} withProviders />
{#if step === OtpLoginSteps.Email}
<Form bind:this={form} caption={login.string.SignUp} {status} {fields} {object} {action} withProviders />
{/if}
{#if step === OtpLoginSteps.Otp && object.username !== ''}
<OtpForm
email={object.username}
{signUpDisabled}
{navigateUrl}
loginState="signup"
retryOn={otpRetryOn}
on:step={handleStep}
/>
{/if}
<div class="action">
<BottomActionComponent action={changeMethodAction} />
</div>
<style lang="scss">
.action {
margin-left: 5rem;
}
</style>
+3 -1
View File
@@ -67,6 +67,8 @@ export default mergeIds(loginId, login, {
SentTo: '' as IntlString,
CanFindCode: '' as IntlString,
LoginWithCode: '' as IntlString,
LoginWithPassword: '' as IntlString
LoginWithPassword: '' as IntlString,
SignUpWithCode: '' as IntlString,
SignUpWithPassword: '' as IntlString
}
})
+8 -31
View File
@@ -222,8 +222,6 @@ export async function performWorkspaceOperation (
operation: WorkspaceUserOperation,
...params: any[]
): Promise<boolean> {
// TODO: this method requires a special admin token
// consider how to obtain it
const token = getMetadata(presentation.metadata.Token)
if (token === undefined) {
const loc = getCurrentLocation()
@@ -247,8 +245,6 @@ export async function performWorkspaceOperation (
}
export async function getAllWorkspaces (): Promise<WorkspaceInfoWithStatus[]> {
// TODO: this method requires a special admin token
// consider how to obtain it
const token = getMetadata(presentation.metadata.Token)
if (token === undefined) {
const loc = getCurrentLocation()
@@ -629,7 +625,7 @@ export async function leaveWorkspace (account: string): Promise<LoginInfo | null
return await getAccountClient().leaveWorkspace(account)
}
export async function sendInvite (email: string, role?: AccountRole): Promise<void> {
export async function sendInvite (email: string, role: AccountRole): Promise<void> {
try {
await getAccountClient().sendInvite(email, role)
} catch (e) {
@@ -638,32 +634,13 @@ export async function sendInvite (email: string, role?: AccountRole): Promise<vo
}
}
export async function resendInvite (email: string): Promise<void> {
// TODO: FIXME
throw new Error('Not implemented')
// const accountsUrl = getMetadata(login.metadata.AccountsUrl)
// if (accountsUrl === undefined) {
// throw new Error('accounts url not specified')
// }
// const token = getMetadata(presentation.metadata.Token) as string
// const params = [email]
// const request = {
// method: 'resendInvite',
// params
// }
// await fetch(accountsUrl, {
// method: 'POST',
// headers: {
// Authorization: 'Bearer ' + token,
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify(request)
// })
export async function resendInvite (email: string, role: AccountRole): Promise<void> {
try {
await getAccountClient().resendInvite(email, role)
} catch (e) {
console.log('Failed to resend invite', email, role)
console.error(e)
}
}
export async function requestPassword (email: string): Promise<Status> {
+2 -2
View File
@@ -64,8 +64,8 @@ export default plugin(loginId, {
WorkspaceArchivedDesc: '' as IntlString
},
function: {
SendInvite: '' as Resource<(email: string, role?: AccountRole) => Promise<void>>,
ResendInvite: '' as Resource<(inviteId: string) => Promise<void>>,
SendInvite: '' as Resource<(email: string, role: AccountRole) => Promise<void>>,
ResendInvite: '' as Resource<(email: string, role: AccountRole) => Promise<void>>,
GetInviteLink: '' as Resource<
(
expHours: number,
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "Nová zpráva",
"MailThread": "Zpráva",
"Reply": "Odpovědět",
"Date": "Datum"
"Date": "Datum",
"MailMessage": "Zpráva",
"MailMessages": "Zprávy"
}
}
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "Neue Nachricht",
"MailThread": "Nachricht",
"Reply": "Antworten",
"Date": "Datum"
"Date": "Datum",
"MailMessage": "Nachricht",
"MailMessages": "Nachrichten"
}
}
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "New Mail",
"MailThread": "Mail",
"Reply": "Reply",
"Date": "Date"
"Date": "Date",
"MailMessage": "Message",
"MailMessages": "Messages"
}
}
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "Nuevo mensaje",
"MailThread": "Mensaje",
"Reply": "Responder",
"Date": "Fecha"
"Date": "Fecha",
"MailMessage": "Mensaje",
"MailMessages": "Mensajes"
}
}
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "Nouveau message",
"MailThread": "Message",
"Reply": "Répondre",
"Date": "Date"
"Date": "Date",
"MailMessage": "Message",
"MailMessages": "Messages"
}
}
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "Nuovo messaggio",
"MailThread": "Messaggio",
"Reply": "Rispondi",
"Date": "Data"
"Date": "Data",
"MailMessage": "Messaggio",
"MailMessages": "Messaggi"
}
}
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "Nova mensagem",
"MailThread": "Mensagem",
"Reply": "Responder",
"Date": "Data"
"Date": "Data",
"MailMessage": "Mensagem",
"MailMessages": "Mensagens"
}
}
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "Новое сообщение",
"MailThread": "Сообщение",
"Reply": "Ответить",
"Date": "Дата"
"Date": "Дата",
"MailMessage": "Сообщение",
"MailMessages": "Сообщения"
}
}
+3 -1
View File
@@ -6,6 +6,8 @@
"CreateMail": "新邮件",
"MailThread": "邮件",
"Reply": "回复",
"Date": "日期"
"Date": "日期",
"MailMessage": "邮件",
"MailMessages": "邮件"
}
}
@@ -15,35 +15,38 @@
//
-->
<script lang="ts">
import core, { Data, Doc, generateId, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import chunter, { type ChatMessage } from '@hcengineering/chunter'
import { Data, Doc, generateId, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import { Card, getClient, isSpace } from '@hcengineering/presentation'
import { MailThread } from '@hcengineering/mail'
import { MailThread, MailMessage } from '@hcengineering/mail'
import { createFocusManager, EditBox, FocusHandler } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import mail from '../plugin'
import { isValidEmail } from '../messageUtils'
import { getEmailSocialId, isNotEmpty, isValidEmail } from '../messageUtils'
const manager = createFocusManager()
const dispatch = createEventDispatcher()
const client = getClient()
const account = getCurrentAccount()
export let space: Ref<Space>
export let mailThreadId: Ref<MailThread> | undefined = undefined
export let to = ''
export let from = getEmail()
// TODO: Add new social type for huly email
export let from = getEmailSocialId(account)
export let subject = ''
let message = ''
$: canSave = isNotEmpty(to) && isValidEmail(to) && (isNotEmpty(subject) || isNotEmpty(message))
export function canClose (): boolean {
return to === '' && from === '' && subject === ''
}
export function getEmail (): string {
// TODO: use email from account
return 'test@huly.me'
function generateMailId (): string {
// TODO: Remove when email send is implemented, should be generated by SMTP server
return `<${generateId()}@test.example.com>`
}
async function createMail (): Promise<void> {
@@ -52,13 +55,13 @@
throw new Error('Failed to create mail thread')
}
const messageId: Ref<ChatMessage> = await client.addCollection<Doc, ChatMessage>(
chunter.class.ChatMessage,
const messageId: Ref<MailMessage> = await client.addCollection<Doc, MailMessage>(
mail.class.MailMessage,
getSpace(mailThread),
mailThread._id,
mail.class.MailThread,
'messages',
{ message: message ?? '' }
{ message: message ?? '', mailId: generateMailId() }
)
dispatch('close', messageId)
@@ -84,7 +87,7 @@
preview: getMessagePreview(message)
}
const threadId = await client.createDoc(mail.class.MailThread, core.space.Space, data)
const threadId = await client.createDoc(mail.class.MailThread, space, data)
return await client.findOne(mail.class.MailThread, { _id: threadId as any })
}
@@ -103,7 +106,7 @@
<Card
label={mail.string.CreateMail}
okAction={createMail}
canSave={to.trim().length > 0 && isValidEmail(to)}
{canSave}
on:close={() => {
dispatch('close')
}}
@@ -21,10 +21,10 @@
import { getResource } from '@hcengineering/platform'
import { ActionContext, getClient } from '@hcengineering/presentation'
import { type Class, type Ref } from '@hcengineering/core'
import mail, { MailThread } from '@hcengineering/mail'
import mail, { MailThread, MailMessage } from '@hcengineering/mail'
import { Panel } from '@hcengineering/panel'
import { type AnySvelteComponent, Button, Component, Loading, showPopup } from '@hcengineering/ui'
import chunter, { type ChatMessage } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter'
import view, { type ObjectPresenter } from '@hcengineering/view'
import { getReplySubject } from '../messageUtils'
@@ -32,8 +32,8 @@
export let _id: Ref<MailThread>
export let _class: Ref<Class<MailThread>>
const messageClass = chunter.class.ChatMessage
let messages: ChatMessage[] = []
const messageClass = mail.class.MailMessage
let messages: MailMessage[] = []
let isLoading = true
let object: MailThread | undefined
@@ -62,7 +62,7 @@
async function findMessagePresenter (): Promise<void> {
const presenterMixin: ObjectPresenter | undefined = getClient()
.getHierarchy()
.classHierarchyMixin(messageClass, view.mixin.ObjectPresenter) as any
.classHierarchyMixin(chunter.class.ChatMessage, view.mixin.ObjectPresenter) as any
if (presenterMixin?.presenter !== undefined) {
messagePresenter = await getResource(presenterMixin.presenter)
}
+14 -1
View File
@@ -13,14 +13,17 @@
// limitations under the License.
//
import {
type Account,
type Client,
type Data,
type Doc,
type Ref,
SocialIdType,
type Space,
type TxOperations,
generateId,
getCurrentAccount
getCurrentAccount,
parseSocialIdString
} from '@hcengineering/core'
import chunter, { type ChatMessage } from '@hcengineering/chunter'
import contact, { getCurrentEmployee } from '@hcengineering/contact'
@@ -104,6 +107,16 @@ export function isValidEmail (email: string): boolean {
return emailRegex.test(email)
}
export function isNotEmpty (str: string | undefined): boolean {
return str !== undefined && str.trim().length > 0
}
export function getReplySubject (subject: string): string {
return subject.startsWith('Re:') ? subject : `Re: ${subject}`
}
export function getEmailSocialId (account: Account): string {
return (
account.socialIds.map((id) => parseSocialIdString(id)).find((it) => it.type === SocialIdType.EMAIL)?.value ?? ''
)
}
+10 -2
View File
@@ -15,7 +15,7 @@
import type { Class, Ref } from '@hcengineering/core'
import { Asset, IntlString, type Plugin, plugin } from '@hcengineering/platform'
import type { ChunterSpace } from '@hcengineering/chunter'
import type { ChunterSpace, ChatMessage } from '@hcengineering/chunter'
import type { AnyComponent } from '@hcengineering/ui'
export interface MailThread extends ChunterSpace {
@@ -26,6 +26,10 @@ export interface MailThread extends ChunterSpace {
preview: string
}
export interface MailMessage extends ChatMessage {
mailId: string
}
/**
* @public
*/
@@ -33,7 +37,8 @@ export const mailId = 'mail' as Plugin
export default plugin(mailId, {
class: {
MailThread: '' as Ref<Class<MailThread>>
MailThread: '' as Ref<Class<MailThread>>,
MailMessage: '' as Ref<Class<MailMessage>>
},
component: {
CreateMail: '' as AnyComponent,
@@ -41,6 +46,9 @@ export default plugin(mailId, {
MailThread: '' as AnyComponent
},
string: {
MailMessage: '' as IntlString,
MailMessages: '' as IntlString,
MailId: '' as IntlString,
MailThread: '' as IntlString,
MailThreadId: '' as IntlString,
MailPreview: '' as IntlString,
@@ -15,6 +15,8 @@
import { showPopup } from '@hcengineering/ui'
import { Extension } from '@tiptap/core'
import { type MarkType } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import LinkPopup from '../LinkPopup.svelte'
export const LinkUtilsExtension = Extension.create<any>({
@@ -42,6 +44,28 @@ export const LinkUtilsExtension = Extension.create<any>({
},
addProseMirrorPlugins () {
return []
return [LinkClickHandlerPlugin({ type: this.editor.schema.marks.link })]
}
})
interface LinkClickHandlerOptions {
type: MarkType
}
export function LinkClickHandlerPlugin (options: LinkClickHandlerOptions): Plugin {
return new Plugin({
key: new PluginKey('handleClickLink'),
props: {
handleClick: (view, pos, event) => {
const $pos = view.state.doc.resolve(pos)
const link = options.type.isInSet($pos.marks())
if (typeof link?.attrs.href === 'string') {
window.open(link.attrs.href, link.attrs.target)
return true
}
return false
}
}
})
}
@@ -363,7 +363,7 @@ export async function getReferenceFromUrl (text: string): Promise<ReferenceNodeP
const _id: Ref<Doc> | undefined =
linkProvider !== undefined ? (await (await getResource(linkProvider.decode))(id)) ?? id : id
const label = await getReferenceLabel(objectclass, id)
const label = await getReferenceLabel(objectclass, _id)
if (label === '') return
return {
@@ -62,7 +62,7 @@ export const DefaultKit = Extension.create<DefaultKitOptions>({
}),
Typography.configure({}),
Link.extend({ inclusive: false }).configure({
openOnClick: true,
openOnClick: false,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
CodeBlockHighlighExtension.configure(codeBlockHighlightOptions)
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space, WithLookup } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space, WithLookup, mergeQueries } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import {
@@ -38,7 +38,7 @@
ViewletSelector,
ViewletSettingButton
} from '@hcengineering/view-resources'
import { ParentsNavigationModel } from '@hcengineering/workbench'
import { QueryOptions, ParentsNavigationModel } from '@hcengineering/workbench'
import ComponentNavigator from './ComponentNavigator.svelte'
@@ -56,6 +56,7 @@
export let baseQuery: DocumentQuery<Doc> | undefined = undefined
export let modes: IModeSelector<any> | undefined = undefined
export let navigationModel: ParentsNavigationModel | undefined = undefined
export let queryOptions: QueryOptions | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -68,7 +69,8 @@
let viewlets: Array<WithLookup<Viewlet>> = []
let viewOptions: ViewOptions | undefined
$: _baseQuery = { ...(baseQuery ?? {}), ...(viewlet?.baseQuery ?? {}) }
$: spaceQuery = queryOptions?.filterBySpace === true && space !== undefined ? { space } : {}
$: _baseQuery = mergeQueries(mergeQueries(baseQuery ?? {}, viewlet?.baseQuery ?? {}), spaceQuery)
$: query = { ..._baseQuery }
$: searchQuery = search === '' ? query : { ...query, $search: search }
$: resultQuery = searchQuery
@@ -996,7 +996,8 @@
currentSpace,
space: currentSpace,
navigationModel: specialComponent?.navigationModel,
workbenchWidth
workbenchWidth,
queryOptions: specialComponent?.queryOptions
}}
on:action={(e) => {
if (e?.detail) {
+9
View File
@@ -184,6 +184,7 @@ export interface SpecialNavModel {
(inboxNotificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => number
>
navigationModel?: ParentsNavigationModel
queryOptions?: QueryOptions
}
/**
@@ -202,6 +203,14 @@ export interface ParentsNavigationModel {
createButton?: AnyComponent
}
/**
* @public
*/
export interface QueryOptions {
// If specified should display only documents from the current space
filterBySpace?: boolean
}
/**
* @public
*/
+11 -6
View File
@@ -34,21 +34,23 @@ const sql: Sql = postgres(dbUrl, {
fetch_types: true
})
async function toResponse (compression: string, data: any, response: http.ServerResponse): Promise<void> {
async function toResponse (compression: string, data: any, response: http.ServerResponse, qtime: number): Promise<void> {
if (compression === 'snappy') {
response
.writeHead(200, {
'content-type': 'application/json',
compression: 'snappy',
'content-encoding': 'snappy',
'keep-alive': 'timeout=5'
'keep-alive': 'timeout=5',
querytime: `${qtime}`
})
.end(await compress(JSON.stringify(data)))
} else {
response
.writeHead(200, {
'content-type': 'application/json',
'keep-alive': 'timeout=5'
'keep-alive': 'timeout=5',
querytime: `${qtime}`
})
.end(JSON.stringify(data))
}
@@ -96,8 +98,9 @@ async function handleSQLFind (
query: json.query
})
const result = await query
console.log('query', json.query, Date.now() - st, result.length)
await toResponse(compression, result, response)
const qtime = Date.now() - st
console.log('query', json.query, qtime, result.length)
await toResponse(compression, result, response, qtime)
} catch (err: any) {
console.error('failed to execute sql', json.query, json.params, err.message, err)
if (!response.writableEnded) {
@@ -122,7 +125,9 @@ const reqHandler = (req: http.IncomingMessage, resp: http.ServerResponse): void
return
}
if (req.method === 'POST' && url.startsWith('/api/v1/sql')) {
void handleSQLFind(compression, req, resp)
void handleSQLFind(compression, req, resp).catch((err) => {
console.error('failed to execute query: ', err)
})
} else {
resp.writeHead(404).end('Not found')
}
@@ -581,7 +581,7 @@ test.describe('QMS. Documents tests', () => {
})
})
test('TESTS-162. Approve document with delayed release', async ({ page }) => {
test.skip('TESTS-162. Approve document with delayed release', async ({ page }) => {
await allure.description('Requirement\nUsers need to create document with delayed release')
await allure.tms('TESTS-162', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-162')
const approveDelayedDocument: NewDocument = {
@@ -32,7 +32,7 @@ test.describe('Registration tests', () => {
await loginPage.buttonSignUp.click()
const signupPage = new SignupPage(page)
await signupPage.signup(signUpUserData)
await signupPage.signupPwd(signUpUserData)
await attachScreenshot('TESTS-143_registration.png', page)
@@ -69,7 +69,7 @@ test.describe('Registration tests', () => {
await loginPage.buttonSignUp.click()
const signupPage = new SignupPage(page)
await signupPage.signup(signUpUserData)
await signupPage.signupPwd(signUpUserData)
await signupPage.buttonSignUp.click()
await expect(signupPage.textError).toHaveText('Account already exists')
+8 -1
View File
@@ -10,6 +10,7 @@ export class SignupPage {
readonly inputRepeatNewPassword: Locator
readonly buttonSignUp: Locator
readonly textError: Locator
readonly signUpPasswordBtn: Locator
constructor (page: Page) {
this.page = page
@@ -20,9 +21,15 @@ export class SignupPage {
this.inputRepeatNewPassword = page.locator('input[name="new-password"]').nth(1)
this.buttonSignUp = page.locator('div.send button')
this.textError = page.locator('div.ERROR > span')
this.signUpPasswordBtn = page.locator('a', { hasText: 'Sign up with password' })
}
async signup (userData: UserSignUp): Promise<void> {
async signupPwd (userData: UserSignUp): Promise<void> {
const isOtp = await this.signUpPasswordBtn.isVisible()
if (isOtp) {
await this.signUpPasswordBtn.click()
}
await this.inputFirstName.fill(userData.firstName)
await this.inputLastName.fill(userData.lastName)
await this.inputEmail.fill(userData.email)
+1 -1
View File
@@ -2284,7 +2284,7 @@
{
"packageName": "@hcengineering/mail",
"projectFolder": "plugins/mail",
"shouldPublish": false
"shouldPublish": true
},
{
"packageName": "@hcengineering/model-mail",
+2 -3
View File
@@ -1,6 +1,5 @@
cd ./dev/tool
rushx run-local create-workspace ws1 -w DevWorkspace # Create workspace
rushx run-local create-account user1 -p 1234 -f John -l Appleseed # Create account
rushx run-local create-workspace ws1 email:user1
rushx run-local configure ws1 --list --enable '*' # Enable all modules, even if they are not yet intended to be used by a wide audience.
rushx run-local assign-workspace user1 ws1 # Assign workspace to user.
rushx run-local confirm-email user1 # To allow the creation of additional test workspaces.
rushx run-local assign-workspace user1 ws1 # Assign workspace to user.
+25 -3
View File
@@ -13,8 +13,8 @@
// limitations under the License.
//
import { AnyAttribute, Tx, TxCreateDoc, TxProcessor } from '@hcengineering/core'
import card from '@hcengineering/card'
import core, { AnyAttribute, Tx, TxCreateDoc, TxProcessor, TxRemoveDoc } from '@hcengineering/core'
import card, { MasterTag } from '@hcengineering/card'
import view from '@hcengineering/view'
import { TriggerControl } from '@hcengineering/server-core'
@@ -47,9 +47,31 @@ async function OnAttribute (ctx: TxCreateDoc<AnyAttribute>[], control: TriggerCo
return []
}
async function OnMasterTagRemove (ctx: TxRemoveDoc<MasterTag>[], control: TriggerControl): Promise<Tx[]> {
const removeTx = ctx[0]
const removedTag = control.removedMap.get(removeTx.objectId)
if (removedTag === undefined) return []
const res: Tx[] = []
// should remove objects if masterTag
if (removedTag._class === card.class.MasterTag) {
const cards = await control.findAll(control.ctx, removeTx.objectId, {})
for (const card of cards) {
res.push(control.txFactory.createTxRemoveDoc(card._class, card.space, card._id))
}
}
const desc = control.hierarchy.getDescendants(removeTx.objectId)
for (const des of desc) {
if (des === removeTx.objectId) continue
res.push(control.txFactory.createTxRemoveDoc(card.class.MasterTag, core.space.Model, des))
}
return res
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
OnAttribute
OnAttribute,
OnMasterTagRemove
}
})
+2 -1
View File
@@ -27,6 +27,7 @@ export const serverCardId = 'server-card' as Plugin
*/
export default plugin(serverCardId, {
trigger: {
OnAttribute: '' as Resource<TriggerFunc>
OnAttribute: '' as Resource<TriggerFunc>,
OnMasterTagRemove: '' as Resource<TriggerFunc>
}
})
+53 -28
View File
@@ -91,7 +91,9 @@ import {
verifyPassword,
wrap,
verifyAllowedServices,
getPersonName
getPersonName,
sendEmail,
getInviteEmail
} from './utils'
import { isAdminEmail } from './admin'
@@ -239,6 +241,8 @@ export async function signUpOtp (
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountAlreadyExists, {}))
}
await db.person.updateOne({ uuid: emailSocialId.personUuid }, { firstName, lastName })
personUuid = emailSocialId.personUuid
} else {
// There's no person linked to this email, so we need to create a new one
@@ -386,7 +390,7 @@ export async function sendInvite (
branding: Branding | null,
token: string,
email: string,
role?: AccountRole
role: AccountRole
): Promise<void> {
const { account, workspace: workspaceUuid } = decodeTokenVerbose(ctx, token)
@@ -400,36 +404,55 @@ export async function sendInvite (
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
}
const { sesURL, sesAuth } = getSesUrl()
const front = getFrontUrl(branding)
const expHours = 48
const exp = expHours * 60 * 60 * 1000
const inviteId = await createInviteLink(ctx, db, branding, token, exp, email, 1, role)
const link = concatLink(front, `/login/join?inviteId=${inviteId}`)
const inviteEmail = await getInviteEmail(branding, email, inviteId, workspace, expHours)
const ws = workspace.name !== '' ? workspace.name : 'workspace'
const lang = branding?.language
const text = await translate(accountPlugin.string.InviteText, { link, ws, expHours }, lang)
const html = await translate(accountPlugin.string.InviteHTML, { link, ws, expHours }, lang)
const subject = await translate(accountPlugin.string.InviteSubject, { ws }, lang)
const to = email
await sendEmail(inviteEmail)
await fetch(concatLink(sesURL, '/send'), {
method: 'post',
headers: {
'Content-Type': 'application/json',
...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {})
},
body: JSON.stringify({
text,
html,
subject,
to
})
ctx.info('Invite has been sent', { to: inviteEmail.to, workspaceUuid: workspace.uuid, workspaceName: workspace.name })
}
export async function resendInvite (
ctx: MeasureContext,
db: AccountDB,
branding: Branding | null,
token: string,
email: string,
role: AccountRole
): Promise<void> {
const { account, workspace: workspaceUuid } = decodeTokenVerbose(ctx, token)
const currentAccount = await db.account.findOne({ uuid: account })
if (currentAccount == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account }))
}
const workspace = await db.workspace.findOne({ uuid: workspaceUuid })
if (workspace == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
}
const expHours = 48
const newExp = Date.now() + expHours * 60 * 60 * 1000
const invite = await db.invite.findOne({ workspaceUuid, emailPattern: email })
let inviteId: string
if (invite != null) {
inviteId = invite.id
await db.invite.updateOne({ id: invite.id }, { expiresOn: newExp, remainingUses: 1, role })
} else {
inviteId = await createInviteLink(ctx, db, branding, token, newExp, email, 1, role)
}
const inviteEmail = await getInviteEmail(branding, email, inviteId, workspace, expHours, true)
await sendEmail(inviteEmail)
ctx.info('Invite has been resent', {
to: inviteEmail.to,
workspaceUuid: workspace.uuid,
workspaceName: workspace.name
})
ctx.info('Invite has been sent', { email, workspace, workspaceName: workspace.name, link })
}
/**
@@ -1522,11 +1545,12 @@ export type AccountMethods =
| 'login'
| 'loginOtp'
| 'signUp'
| 'signUpOTP'
| 'signUpOtp'
| 'validateOtp'
| 'createWorkspace'
| 'createInviteLink'
| 'sendInvite'
| 'resendInvite'
| 'selectWorkspace'
| 'join'
| 'checkJoin'
@@ -1567,11 +1591,12 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
login: wrap(login),
loginOtp: wrap(loginOtp),
...(hasSignUp ? { signUp: wrap(signUp) } : {}),
...(hasSignUp ? { signUpOTP: wrap(signUpOtp) } : {}),
...(hasSignUp ? { signUpOtp: wrap(signUpOtp) } : {}),
validateOtp: wrap(validateOtp),
createWorkspace: wrap(createWorkspace),
createInviteLink: wrap(createInviteLink),
sendInvite: wrap(sendInvite),
resendInvite: wrap(resendInvite),
selectWorkspace: wrap(selectWorkspace),
join: wrap(join),
checkJoin: wrap(checkJoin),
+58
View File
@@ -1139,3 +1139,61 @@ export function getPersonName (person: Person): string {
// Should we control the order by config?
return `${person.firstName} ${person.lastName}`
}
interface EmailInfo {
text: string
html: string
subject: string
to: string
}
export async function sendEmail (info: EmailInfo): Promise<void> {
const { text, html, subject, to } = info
const { sesURL, sesAuth } = getSesUrl()
await fetch(concatLink(sesURL, '/send'), {
method: 'post',
headers: {
'Content-Type': 'application/json',
...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {})
},
body: JSON.stringify({
text,
html,
subject,
to
})
})
}
export async function getInviteEmail (
branding: Branding | null,
email: string,
inviteId: string,
workspace: Workspace,
expHours: number,
resend = false
): Promise<EmailInfo> {
const front = getFrontUrl(branding)
const link = concatLink(front, `/login/join?inviteId=${inviteId}`)
const ws = workspace.name !== '' ? workspace.name : workspace.url
const lang = branding?.language
return {
text: await translate(
resend ? accountPlugin.string.ResendInviteText : accountPlugin.string.InviteText,
{ link, ws, expHours },
lang
),
html: await translate(
resend ? accountPlugin.string.ResendInviteHTML : accountPlugin.string.InviteHTML,
{ link, ws, expHours },
lang
),
subject: await translate(
resend ? accountPlugin.string.ResendInviteSubject : accountPlugin.string.InviteSubject,
{ ws },
lang
),
to: email
}
}
+45 -9
View File
@@ -35,7 +35,10 @@ class GreenClient implements DBClient {
readonly url: string,
private readonly token: string,
private readonly connection: postgres.Sql,
private readonly decoder: ((data: any) => Promise<any>) | undefined
private readonly compression?: {
decoder: (data: any) => Promise<any>
compression: string
}
) {
this.endpoint = concatLink(url, '/api/v1/sql')
}
@@ -47,12 +50,14 @@ class GreenClient implements DBClient {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const st = Date.now()
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token,
Connection: 'keep-alive'
Connection: 'keep-alive',
...(this.compression?.compression !== undefined ? { compression: this.compression.compression } : {})
},
body: JSON.stringify({
query,
@@ -62,15 +67,43 @@ class GreenClient implements DBClient {
if (!response.ok) {
throw new Error(`Failed to execute sql: ${response.status} ${response.statusText}`)
}
if (this.decoder !== undefined && response.headers.get('compression') !== undefined) {
return JSON.parse(await this.decoder(Buffer.from(await response.arrayBuffer())))
}
let size = 0
let encodedSize = 0
try {
if (
this.compression?.decoder !== undefined &&
response.headers.get('compression') === this.compression.compression
) {
const buffer = Buffer.from(await response.arrayBuffer())
encodedSize = buffer.length
const decoded = await this.compression.decoder(buffer)
size = decoded.length
return JSON.parse(decoded)
}
return await response.json()
return await response.json()
} finally {
const qtime = response.headers.get('querytime')
const time = Date.now() - st
console.info({
message: `green query: ${time} ${qtime ?? 0}`,
query,
time,
parameters,
qtime: response.headers.get('querytime'),
size,
encodedSize
})
}
} catch (err: any) {
lastError = err
if (attempt === maxRetries - 1) {
console.warn('green failed after retries', query)
console.warn({
message: 'green failed after retries',
query,
errMessage: err.message,
endpoint: this.endpoint
})
return await this.connection.unsafe(query, params, getPrepare())
}
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100))
@@ -95,7 +128,10 @@ export function createGreenDBClient (
url: string,
token: string,
connection: postgres.Sql,
decoder?: (data: any) => Promise<any>
compression?: {
decoder: (data: any) => Promise<any>
compression: string
}
): DBClient {
return new GreenClient(url, token, connection, decoder)
return new GreenClient(url, token, connection, compression)
}
+22 -17
View File
@@ -62,12 +62,12 @@ import core, {
type WorkspaceUuid
} from '@hcengineering/core'
import {
calcHashHash,
type DbAdapter,
type DbAdapterHandler,
type DomainHelperOperations,
type ServerFindOptions,
type TxAdapter,
calcHashHash
type TxAdapter
} from '@hcengineering/server-core'
import type postgres from 'postgres'
import { createDBClient, createGreenDBClient, type DBClient } from './client'
@@ -425,6 +425,10 @@ abstract class PostgresAdapterBase implements DbAdapter {
}
reserveContext (id: string): () => void {
if (greenURL != null) {
// Do not reserve connection if using green
return () => {}
}
const conn = this.mgr.getConnection(id, true)
return () => {
conn.released = true
@@ -2096,28 +2100,31 @@ export async function createPostgresAdapter (
)
}
let greenDecoder: ((data: any) => Promise<any>) | undefined
let greenURL: string | undefined
let useGreenCompression: string | undefined
function toGreenClient (url: string, connection: postgres.Sql): DBClient {
const originalUrl = new URL(url)
// Extract components with default values if needed
const token = originalUrl.searchParams.get('token') ?? 'secret'
const compression = originalUrl.searchParams.get('compression') ?? ''
// Manually build the new URL components
const newHost = originalUrl.host
const newPathname = originalUrl.pathname
// Construct new search parameters without previous ones
const newSearchParams = new URLSearchParams()
// Add any search parameters you need, like `token` and `compression` if desired
if (compression !== '') {
newSearchParams.set('compression', compression)
}
console.warn('USE GREEN', newHost, newPathname, newSearchParams.toString())
console.warn('USE GREEN', newHost, newPathname)
// Construct the new URL
const newUrl = `${originalUrl.protocol}//${newHost}${newPathname}${newSearchParams.size > 0 ? '?' + newSearchParams.toString() : ''}`
return createGreenDBClient(newUrl, token, connection, greenDecoders.get(compression))
const newUrl = `${originalUrl.protocol}//${newHost}${newPathname}`
return createGreenDBClient(
newUrl,
token,
connection,
useGreenCompression !== undefined && greenDecoder !== undefined
? { decoder: greenDecoder, compression: useGreenCompression }
: undefined
)
}
/**
* @public
@@ -2143,11 +2150,9 @@ export async function createPostgresTxAdapter (
)
}
const greenDecoders = new Map<string, (data: any) => Promise<any>>()
let greenURL: string | undefined
export function registerGreenDecoder (name: string, decoder: (data: any) => Promise<any>): void {
greenDecoders.set(name, decoder)
greenDecoder = decoder
useGreenCompression = name
}
export function registerGreenUrl (url?: string): void {
greenURL = url
+1 -4
View File
@@ -49,7 +49,6 @@ import {
type Workspace
} from '@hcengineering/server-core'
import { type Token } from '@hcengineering/server-token'
import { handleSend } from './utils'
const useReserveContext = (process.env.USE_RESERVE_CTX ?? 'true') === 'true'
@@ -224,9 +223,7 @@ export class ClientSession implements Session {
this.useCompression
)
} else {
void handleSend(ctx, socket, { result: tx }, 1024 * 1024, this.binaryMode, this.useCompression).catch((err) => {
ctx.error('failed to broadcast', err)
})
socket.send(ctx, { result: tx }, this.binaryMode, this.useCompression)
}
}
+4 -58
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { WorkspaceUuid, type FindResult, type MeasureContext } from '@hcengineering/core'
import { WorkspaceUuid, type MeasureContext } from '@hcengineering/core'
import type {
AddSessionActive,
@@ -23,7 +23,6 @@ import type {
Session
} from '@hcengineering/server-core'
import { toFindResult } from '@hcengineering/core'
import { type Response } from '@hcengineering/rpc'
import type { Token } from '@hcengineering/server-token'
@@ -79,65 +78,12 @@ export function processRequest (
}
}
}
export async function sendResponse (
export function sendResponse (
ctx: MeasureContext,
session: Session,
socket: ConnectionSocket,
resp: Response<any>
): Promise<void> {
await handleSend(ctx, socket, resp, 1024 * 1024, session.binaryMode, session.useCompression)
}
function waitNextTick (): Promise<void> | undefined {
return new Promise<void>((resolve) => {
setImmediate(resolve)
})
}
export async function handleSend (
ctx: MeasureContext,
ws: ConnectionSocket,
msg: Response<any>,
chunkLimit: number,
useBinary: boolean,
useCompression: boolean
): Promise<void> {
// ws.send(msg)
if (Array.isArray(msg.result) && msg.result.length > 1 && chunkLimit > 0) {
// Split and send by chunks
const data = [...msg.result]
let cid = 1
const dataSize = JSON.stringify(data).length
const avg = Math.round(dataSize / data.length)
const itemChunk = Math.round(chunkLimit / avg) + 1
while (data.length > 0 && !ws.isClosed) {
let itemChunkCurrent = itemChunk
if (data.length - itemChunk < itemChunk / 2) {
itemChunkCurrent = data.length
}
const chunk: FindResult<any> = toFindResult(data.splice(0, itemChunkCurrent))
if (data.length === 0) {
const orig = msg.result as FindResult<any>
chunk.total = orig.total ?? 0
chunk.lookupMap = orig.lookupMap
}
if (chunk !== undefined) {
ws.send(
ctx,
{ ...msg, result: chunk, chunk: { index: cid, final: data.length === 0 } },
useBinary,
useCompression
)
}
cid++
if (data.length > 0 && !ws.isClosed) {
await waitNextTick()
}
}
} else {
ws.send(ctx, msg, useBinary, useCompression)
}
socket.send(ctx, resp, session.binaryMode, session.useCompression)
return Promise.resolve()
}
+6 -2
View File
@@ -36,7 +36,7 @@ import {
} from '@hcengineering/server'
import {
getClient as getAccountClientRaw,
type WorkspaceLoginInfo,
isWorkspaceLoginInfo,
type AccountClient
} from '@hcengineering/account-client'
import {
@@ -95,7 +95,11 @@ export function startHttpServer (
}
async function getWorkspaceIds (token: string): Promise<WorkspaceIds> {
const wsLoginInfo = (await getAccountClient(token).getLoginInfoByToken()) as WorkspaceLoginInfo
const wsLoginInfo = await getAccountClient(token).getLoginInfoByToken()
if (!isWorkspaceLoginInfo(wsLoginInfo)) {
throw new Error('Invalid workspace login info by token')
}
return {
uuid: wsLoginInfo.workspace,
@@ -16,7 +16,7 @@ import { generateToken, Token } from '@hcengineering/server-token'
import { AnalyticEvent } from '@hcengineering/analytics-collector'
import { AccountRole, MeasureContext, isWorkspaceCreating, WorkspaceUuid, PersonUuid } from '@hcengineering/core'
import { Person } from '@hcengineering/contact'
import { getClient as getAccountClient, WorkspaceLoginInfo } from '@hcengineering/account-client'
import { getClient as getAccountClient, isWorkspaceLoginInfo } from '@hcengineering/account-client'
import { Db, Collection } from 'mongodb'
import { WorkspaceClient } from './workspaceClient'
@@ -159,7 +159,12 @@ export class Collector {
const token = generateToken(account, workspace, { service: 'analytics-collector' })
const wsLoginInfo = await getAccountClient(config.AccountsUrl, token).getLoginInfoByToken()
if ((wsLoginInfo as WorkspaceLoginInfo).role !== AccountRole.Owner) {
if (!isWorkspaceLoginInfo(wsLoginInfo)) {
this.ctx.error('Cannot find workspace login info by token', { wsLoginInfo })
return
}
if (wsLoginInfo.role !== AccountRole.Owner) {
return
}
+7 -3
View File
@@ -18,7 +18,11 @@ import serverClient from '@hcengineering/server-client'
import { initStatisticsContext, StorageConfig, StorageConfiguration } from '@hcengineering/server-core'
import { storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken, { decodeToken } from '@hcengineering/server-token'
import { getClient as getAccountClientRaw, WorkspaceLoginInfo, type AccountClient } from '@hcengineering/account-client'
import {
getClient as getAccountClientRaw,
isWorkspaceLoginInfo,
type AccountClient
} from '@hcengineering/account-client'
import { RoomMetadata, TranscriptionStatus, MeetingMinutes } from '@hcengineering/love'
import cors from 'cors'
import express from 'express'
@@ -144,8 +148,8 @@ export const main = async (): Promise<void> => {
const meetingMinutes = req.body.meetingMinutes
try {
const wsLoginInfo = (await getAccountClient(token).getLoginInfoByToken()) as WorkspaceLoginInfo
if (wsLoginInfo?.workspace == null) {
const wsLoginInfo = await getAccountClient(token).getLoginInfoByToken()
if (!isWorkspaceLoginInfo(wsLoginInfo)) {
console.error('No workspace found for the token')
res.status(401).send()
return
+3 -3
View File
@@ -17,7 +17,7 @@
import { generateId, type WorkspaceIds } from '@hcengineering/core'
import { StorageConfiguration, initStatisticsContext } from '@hcengineering/server-core'
import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { getClient as getAccountClientRaw, AccountClient, WorkspaceLoginInfo } from '@hcengineering/account-client'
import { getClient as getAccountClientRaw, AccountClient, isWorkspaceLoginInfo } from '@hcengineering/account-client'
import cors from 'cors'
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
import { IncomingHttpHeaders, type Server } from 'http'
@@ -104,8 +104,8 @@ const handleRequest = async (
): Promise<void> => {
try {
const token = extractToken(req.headers, req.query)
const wsLoginInfo = (await getAccountClient(token).getLoginInfoByToken()) as WorkspaceLoginInfo
if (wsLoginInfo?.workspace === undefined) {
const wsLoginInfo = await getAccountClient(token).getLoginInfoByToken()
if (!isWorkspaceLoginInfo(wsLoginInfo)) {
throw new ApiError(401, "Couldn't find workspace with the provided token")
}
const wsIds = {
+3 -3
View File
@@ -17,7 +17,7 @@
import { generateId, type WorkspaceIds } from '@hcengineering/core'
import { initStatisticsContext, StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { getClient as getAccountClientRaw, AccountClient, WorkspaceLoginInfo } from '@hcengineering/account-client'
import { getClient as getAccountClientRaw, AccountClient, isWorkspaceLoginInfo } from '@hcengineering/account-client'
import cors from 'cors'
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
import { type Server } from 'http'
@@ -49,8 +49,8 @@ const handleRequest = async (
): Promise<void> => {
try {
const { rawToken } = extractToken(req.headers, req.query)
const wsLoginInfo = (await getAccountClient(rawToken).getLoginInfoByToken()) as WorkspaceLoginInfo
if (wsLoginInfo?.workspace === undefined) {
const wsLoginInfo = await getAccountClient(rawToken).getLoginInfoByToken()
if (!isWorkspaceLoginInfo(wsLoginInfo)) {
throw new ApiError(401, "Couldn't find workspace with the provided token")
}
const wsIds = {
+1 -1
View File
@@ -355,7 +355,7 @@ test.describe('Fulltext index', () => {
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
})
+6 -1
View File
@@ -10,6 +10,7 @@ export class SignUpPage extends CommonPage {
this.page = page
}
signUpPasswordBtn = (): Locator => this.page.locator('a', { hasText: 'Sign up with password' })
inputFirstName = (): Locator => this.page.locator('input[name="given-name"]')
inputLastName = (): Locator => this.page.locator('input[name="family-name"]')
inputEmail = (): Locator => this.page.locator('input[name="email"]')
@@ -42,7 +43,11 @@ export class SignUpPage extends CommonPage {
await this.buttonSignUp().click()
}
async signUp (data: SignUpData, mode: 'join' | 'signup' = 'signup'): Promise<void> {
async signUpPwd (data: SignUpData, mode: 'join' | 'signup' = 'signup'): Promise<void> {
const isOtp = await this.signUpPasswordBtn().isVisible()
if (isOtp) {
await this.signUpPasswordBtn().click()
}
await this.enterFirstName(data.firstName)
await this.enterLastName(data.lastName)
await this.enterEmail(data.email)
+7 -6
View File
@@ -38,7 +38,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.clickTracker()
})
@@ -67,7 +67,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Issue Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await trackerNavigationMenuPage.openIssuesForProject('Default')
@@ -91,6 +91,7 @@ test.describe('Workspace tests', () => {
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUpPasswordBtn().click()
await signUpPage.checkInfo(page, 'Required field First name')
await signUpPage.enterFirstName(newUser.firstName)
await signUpPage.checkInfo(page, 'Required field Last name')
@@ -118,7 +119,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.clickTracker()
@@ -141,7 +142,7 @@ test.describe('Workspace tests', () => {
await page2.getByRole('link', { name: 'Sign Up' }).click()
const signUpPage2 = new SignUpPage(page2)
await signUpPage2.signUp(newUser2, 'join')
await signUpPage2.signUpPwd(newUser2, 'join')
const leftSideMenuPage2 = new LeftSideMenuPage(page2)
await leftSideMenuPage2.clickTracker()
@@ -160,7 +161,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.clickTracker()
@@ -184,7 +185,7 @@ test.describe('Workspace tests', () => {
}
const signUpPage2 = new SignUpPage(page2)
await signUpPage2.signUp(newUser2)
await signUpPage2.signUpPwd(newUser2)
// Ok we signed in, and no workspace present.
await page2.goto(linkText ?? '')
@@ -44,7 +44,7 @@ test.describe.skip('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.clickTracker()
await issuesPage.checkIssuesCount('Hello and Welcome to Huly! 🌟', 1)
@@ -67,7 +67,7 @@ test.describe.skip('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
@@ -106,7 +106,7 @@ test.describe.skip('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.clickNotification()
await notificationPage.clickOnNotification('HI-1')
@@ -123,7 +123,7 @@ test.describe.skip('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.buttonTracker().click()
await userProfilePage.openProfileMenu()
@@ -145,7 +145,7 @@ test.describe.skip('Workspace tests', () => {
const newWorkspaceName2 = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await api.createWorkspaceWithLogin(newWorkspaceName2, newUser.email, '1234')
await userProfilePage.openProfileMenu()
@@ -167,7 +167,7 @@ test.describe.skip('Workspace tests', () => {
const newWorkspaceName2 = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await userProfilePage.openProfileMenu()
await userProfilePage.clickSettings()
@@ -40,7 +40,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await userProfilePage.openProfileMenu()
await userProfilePage.clickSettings()
@@ -58,7 +58,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await userProfilePage.openProfileMenu()
await userProfilePage.clickSettings()
@@ -76,7 +76,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await userProfilePage.openProfileMenu()
await userProfilePage.clickSettings()
@@ -98,7 +98,7 @@ test.describe('Workspace tests', () => {
const newTemplateName = faker.word.words(2)
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await userProfilePage.openProfileMenu()
await userProfilePage.clickSettings()
@@ -116,7 +116,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await userProfilePage.openProfileMenu()
await userProfilePage.clickSettings()
@@ -136,7 +136,7 @@ test.describe('Workspace tests', () => {
const enumName = faker.word.words(2)
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await userProfilePage.openProfileMenu()
await userProfilePage.clickSettings()
@@ -157,7 +157,7 @@ test.describe('Workspace tests', () => {
const enumName = faker.word.words(2)
await loginPage.goto()
await loginPage.linkSignUp().click()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await userProfilePage.openProfileMenu()
await userProfilePage.clickSettings()
+10 -16
View File
@@ -1,17 +1,17 @@
// Copyright © 2024 Huly Labs.
import {
type Account,
generateId,
NoMetricsContext,
WorkspaceUuid,
type Account,
type Class,
type Doc,
type DocumentQuery,
type FindOptions,
type MeasureContext,
type Ref,
type Tx,
WorkspaceUuid
type Tx
} from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import { RPCHandler } from '@hcengineering/rpc'
@@ -39,7 +39,6 @@ import {
createPostgreeDestroyAdapter,
createPostgresAdapter,
createPostgresTxAdapter,
getDBClient,
registerGreenDecoder,
registerGreenUrl,
setDBExtraOptions
@@ -86,13 +85,12 @@ export class Transactor extends DurableObject<Env> {
setDBExtraOptions({
ssl: false,
max: env.USE_GREEN === 'true' ? 2 : 5, // Cloud flare limit an concurrent connection to be 6 total
connection: {
application_name: 'cloud-transactor'
}
})
console.log({ message: 'wakeup', connections: ctx.getWebSockets().length })
// this.ctx.setHibernatableWebSocketEventTimeout(60 * 1000)
this.ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(pingConst, pongConst))
@@ -115,7 +113,7 @@ export class Transactor extends DurableObject<Env> {
if (env.USE_GREEN === 'true') {
registerGreenUrl(env.GREEN_URL)
registerGreenDecoder('snappy', uncompress)
registerGreenDecoder('snappy', async (data) => uncompress(data))
}
registerStringLoaders()
@@ -149,19 +147,15 @@ export class Transactor extends DurableObject<Env> {
extraLogging: true,
pipelineContextVars: this.contextVars
})
const result = await pipeline(ctx, ws, upgrade, broadcast, branding)
const client = getDBClient(this.contextVars, dbUrl)
const connection = await client.getClient()
const t1 = Date.now()
await connection`select now()`
console.log('DB query time', Date.now() - t1)
client.close()
return result
return await pipeline(ctx, ws, upgrade, broadcast, branding)
}
void this.ctx
.blockConcurrencyWhile(async () => {
const wakeUps = ((await ctx.storage.get('wakeUps')) as number) ?? 0
console.log({ message: `wakeup ${wakeUps}`, connections: ctx.getWebSockets().length })
await ctx.storage.put('wakeUps', wakeUps + 1)
this.sessionManager = createSessionManager(
this.measureCtx,
(token: Token, workspace: Workspace, account: Account) => new ClientSession(token, workspace, account, false),
+2 -2
View File
@@ -13,8 +13,8 @@ head_sampling_rate = 1 # optional. default = 1.
# If you are running back-end logic in a Worker, running it closer to your back-end infrastructure
# rather than the end user may result in better performance.
# Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
#[placement]
#mode = "smart"
[placement]
mode = "smart"
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
# Docs:
@@ -44,7 +44,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Workspace Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.clickTracker()
})
@@ -72,7 +72,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `New Issue Name - ${generateId(2)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await trackerNavigationMenuPage.openIssuesForProject('Default')
@@ -96,6 +96,7 @@ test.describe('Workspace tests', () => {
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUpPasswordBtn().click()
await signUpPage.checkInfo(page, 'Required field First name')
await signUpPage.enterFirstName(newUser.firstName)
await signUpPage.checkInfo(page, 'Required field Last name')
@@ -123,7 +124,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.clickTracker()
@@ -146,7 +147,7 @@ test.describe('Workspace tests', () => {
await page2.getByRole('link', { name: 'Sign Up' }).click()
const signUpPage2 = new SignUpPage(page2)
await signUpPage2.signUp(newUser2, 'join')
await signUpPage2.signUpPwd(newUser2, 'join')
const leftSideMenuPage2 = new LeftSideMenuPage(page2)
await leftSideMenuPage2.clickTracker()
@@ -165,7 +166,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await leftSideMenuPage.clickTracker()
@@ -189,7 +190,7 @@ test.describe('Workspace tests', () => {
}
const signUpPage2 = new SignUpPage(page2)
await signUpPage2.signUp(newUser2)
await signUpPage2.signUpPwd(newUser2)
// Ok we signed in, and no workspace present.
await page2.goto(linkText ?? '')
@@ -213,7 +214,7 @@ test.describe('Workspace tests', () => {
const newWorkspaceName = `Some HULY #@$ WS - ${generateId(12)}`
await loginPage.goto()
await loginPage.clickSignUp()
await signUpPage.signUp(newUser)
await signUpPage.signUpPwd(newUser)
await selectWorkspacePage.createWorkspace(newWorkspaceName)
await trackerNavigationMenuPage.checkIfTrackerSidebarIsVisible()
await userProfilePage.openProfileMenu()