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

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2024-10-16 21:15:16 +07:00
140 changed files with 1993 additions and 398 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
A clear and concise description of the issue.
### Your environment
* Version of huly
* Version of Huly
* Browser (and version)
* Your operating system (and version)
+3 -1
View File
@@ -95,9 +95,11 @@
"ACCOUNT_PORT": "3000",
"FRONT_URL": "http://localhost:8080",
"SES_URL": "",
// "WS_LIVENESS_DAYS": "1",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost"
"MINIO_ENDPOINT": "localhost",
// "DISABLE_SIGNUP": "true",
// "INIT_SCRIPT_URL": "https://raw.githubusercontent.com/hcengineering/init/main/script.yaml",
// "INIT_WORKSPACE": "onboarding",
},
+3
View File
@@ -83,8 +83,10 @@ services:
- RESERVED_DB_NAMES=telegram,gmail,github
- MODEL_ENABLED=*
- LAST_NAME_FIRST=true
# - WS_LIVENESS_DAYS=1
- ACCOUNTS_URL=http://host.docker.internal:3000
- BRANDING_PATH=/var/cfg/branding.json
# - DISABLE_SIGNUP=true
# - INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml
# - INIT_WORKSPACE=onboarding
restart: unless-stopped
@@ -197,6 +199,7 @@ services:
- DESKTOP_UPDATES_URL=https://dist.huly.io
- DESKTOP_UPDATES_CHANNEL=dev
- BRANDING_URL=http://host.docker.internal:8087/branding.json
# - DISABLE_SIGNUP=true
restart: unless-stopped
transactor:
image: hardcoreeng/transactor
+34 -24
View File
@@ -54,6 +54,10 @@ export async function moveFromMongoToPG (
client.close()
}
function escapeBackticks (str: string): string {
return str.replaceAll("'", "''")
}
async function moveWorkspace (
accountDb: AccountDB,
mongo: MongoClient,
@@ -77,32 +81,38 @@ async function moveWorkspace (
for (const collection of collections) {
const cursor = collection.find()
const domain = translateDomain(collection.collectionName)
const current = await pgClient.query(`SELECT _id FROM ${domain} WHERE "workspaceId" = $1`, [ws.workspace])
const currentIds = new Set(current.rows.map((r) => r._id))
console.log('move domain', domain)
const docs: Doc[] = []
while (true) {
const doc = (await cursor.next()) as Doc | null
if (doc === null) break
try {
const converted = convertDoc(doc, ws.workspaceName ?? ws.workspace)
await retryTxn(pgClient, async (client) => {
await client.query(
`INSERT INTO ${domain} (_id, "workspaceId", _class, "createdBy", "modifiedBy", "modifiedOn", "createdOn", space, "attachedTo", data) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[
converted._id,
converted.workspaceId,
converted._class,
converted.createdBy ?? converted.modifiedBy,
converted.modifiedBy,
converted.modifiedOn,
converted.createdOn ?? converted.modifiedOn,
converted.space,
converted.attachedTo,
converted.data
]
)
})
} catch (err) {
console.log('error when move doc', doc._id, doc._class, err)
continue
while (docs.length < 50000) {
const doc = (await cursor.next()) as Doc | null
if (doc === null) break
if (currentIds.has(doc._id)) continue
docs.push(doc)
}
if (docs.length === 0) break
while (docs.length > 0) {
const part = docs.splice(0, 500)
const vals = part
.map((doc) => {
const d = convertDoc(doc, ws.workspace)
return `('${d._id}', '${d.workspaceId}', '${d._class}', '${d.createdBy ?? d.modifiedBy}', '${d.modifiedBy}', ${d.modifiedOn}, ${d.createdOn ?? d.modifiedOn}, '${d.space}', ${
d.attachedTo != null ? `'${d.attachedTo}'` : 'NULL'
}, '${escapeBackticks(JSON.stringify(d.data))}')`
})
.join(', ')
try {
await retryTxn(pgClient, async (client) => {
await client.query(
`INSERT INTO ${translateDomain(domain)} (_id, "workspaceId", _class, "createdBy", "modifiedBy", "modifiedOn", "createdOn", space, "attachedTo", data) VALUES ${vals}`
)
})
} catch (err) {
console.log('error when move doc to', domain, err)
continue
}
}
}
}
+2 -2
View File
@@ -1598,7 +1598,7 @@ export function devTool (
throw new Error('mongodbUri is not set')
}
await withDatabase(dbUrl, async (db) => {
await withDatabase(mongodbUri, async (db) => {
const workspaces = await listWorkspacesRaw(db)
workspaces.sort((a, b) => b.lastVisit - a.lastVisit)
await moveFromMongoToPG(
@@ -1617,7 +1617,7 @@ export function devTool (
throw new Error('mongodbUri is not set')
}
await withDatabase(dbUrl, async (db) => {
await withDatabase(mongodbUri, async (db) => {
const workspaceInfo = await getWorkspaceById(db, workspace)
if (workspaceInfo === null) {
throw new Error(`workspace ${workspace} not found`)
+1 -1
View File
@@ -112,7 +112,7 @@ export async function recreateElastic (
const db = getWorkspaceMongoDB(_client, workspaceId)
await db
.collection(DOMAIN_DOC_INDEX_STATE)
.updateMany({ _class: core.class.DocIndexState }, { $set: { stages: {} } })
.updateMany({ _class: core.class.DocIndexState }, { $set: { stages: {}, needIndex: true } })
await connection.sendForceClose()
} finally {
client.close()
+2 -1
View File
@@ -955,7 +955,8 @@ export function createModel (builder: Builder): void {
)
// Allow to use fuzzy search for mixins
builder.mixin(contact.class.Contact, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: contact.class.Contact,
fullTextSummary: true
})
+6 -3
View File
@@ -549,7 +549,8 @@ export function createModel (builder: Builder): void {
func: documents.function.GetAllDocumentStates
})
builder.mixin(documents.class.Document, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: documents.class.Document,
fullTextSummary: true,
childProcessingAllowed: true
})
@@ -886,11 +887,13 @@ export function defineNotifications (builder: Builder): void {
}
export function defineSearch (builder: Builder): void {
builder.mixin(documents.class.Document, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: documents.class.Document,
parentPropagate: true
})
builder.mixin(documents.class.DocumentMeta, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: documents.class.DocumentMeta,
fullTextSummary: true,
childProcessingAllowed: true,
propagate: []
+4 -2
View File
@@ -361,8 +361,10 @@ export class TDocIndexState extends TDoc implements DocIndexState {
generationId?: string
}
@MMixin(core.mixin.FullTextSearchContext, core.class.Class)
export class TFullTextSearchContext extends TClass implements FullTextSearchContext {}
@Model(core.class.FullTextSearchContext, core.class.Doc, DOMAIN_MODEL)
export class TFullTextSearchContext extends TDoc implements FullTextSearchContext {
toClass!: Ref<Class<Doc<Space>>>
}
@MMixin(core.mixin.ConfigurationElement, core.class.Class)
export class TConfigurationElement extends TClass implements ConfigurationElement {
+2 -1
View File
@@ -309,7 +309,8 @@ export function createModel (builder: Builder): void {
]
})
builder.mixin(core.class.Space, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: core.class.Space,
childProcessingAllowed: false
})
+5 -1
View File
@@ -262,7 +262,11 @@ export const coreOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
// We need to delete all documents in doc index state for missing classes
const allClasses = client.hierarchy.getDescendants(core.class.Doc)
const allIndexed = allClasses.filter((it) => isClassIndexable(client.hierarchy, it))
const contexts = new Map(
client.model.findAllSync(core.class.FullTextSearchContext, {}).map((it) => [it.toClass, it])
)
const allIndexed = allClasses.filter((it) => isClassIndexable(client.hierarchy, it, contexts))
// Next remove all non indexed classes and missing classes as well.
await client.update(
+2 -1
View File
@@ -214,7 +214,8 @@ export function createModel (builder: Builder): void {
gmail.action.WriteEmail
)
builder.mixin(gmail.class.Message, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: gmail.class.Message,
parentPropagate: false
})
+6 -3
View File
@@ -1438,13 +1438,15 @@ export function createModel (builder: Builder): void {
)
// Allow to use fuzzy search for mixins
builder.mixin(recruit.class.Vacancy, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: recruit.class.Vacancy,
fullTextSummary: true,
childProcessingAllowed: true,
propagate: []
})
builder.mixin(recruit.mixin.Candidate, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: recruit.mixin.Candidate,
fullTextSummary: true,
propagate: [recruit.class.Applicant],
childProcessingAllowed: true,
@@ -1457,7 +1459,8 @@ export function createModel (builder: Builder): void {
})
// Allow to use fuzzy search for mixins
builder.mixin(recruit.class.Applicant, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: recruit.class.Applicant,
fullTextSummary: true,
forceIndex: true,
childProcessingAllowed: true,
+2 -1
View File
@@ -179,7 +179,8 @@ export function createModel (builder: Builder): void {
telegram.ids.TelegramMessageSharedActivityViewlet
)
builder.mixin(telegram.class.Message, core.class.Class, core.mixin.FullTextSearchContext, {
builder.createDoc(core.class.FullTextSearchContext, core.space.Model, {
toClass: telegram.class.Message,
parentPropagate: false,
childProcessingAllowed: true
})
+9
View File
@@ -331,6 +331,15 @@ export function createModel (builder: Builder): void {
index: 10
})
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
kind: 'image',
action: textEditor.function.DownloadImage,
icon: textEditor.icon.Download,
label: textEditor.string.Download,
category: 90,
index: 15
})
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
kind: 'image',
action: textEditor.function.MoreImageActions,
+1
View File
@@ -29,6 +29,7 @@ export default mergeIds(textEditorId, textEditor, {
ExpandImage: '' as Resource<TextActionFunction>,
MoreImageActions: '' as Resource<TextActionFunction>,
ConfigureNote: '' as Resource<TextActionFunction>,
DownloadImage: '' as Resource<TextActionFunction>,
IsEditableTableActive: '' as Resource<TextActionVisibleFunction>,
IsEditableNote: '' as Resource<TextActionVisibleFunction>,
+2 -1
View File
@@ -578,7 +578,8 @@ export interface BlobLookup extends Blob {
*
* If defined for class, this class will be enabled for embedding search like openai.
*/
export interface FullTextSearchContext extends Class<Doc> {
export interface FullTextSearchContext extends Doc {
toClass: Ref<Class<Doc>>
fullTextSummary?: boolean
forceIndex?: boolean
+2 -2
View File
@@ -142,10 +142,10 @@ export default plugin(coreId, {
StatusCategory: '' as Ref<Class<StatusCategory>>,
MigrationState: '' as Ref<Class<MigrationState>>,
BenchmarkDoc: '' as Ref<Class<BenchmarkDoc>>
BenchmarkDoc: '' as Ref<Class<BenchmarkDoc>>,
FullTextSearchContext: '' as Ref<Mixin<FullTextSearchContext>>
},
mixin: {
FullTextSearchContext: '' as Ref<Mixin<FullTextSearchContext>>,
ConfigurationElement: '' as Ref<Mixin<ConfigurationElement>>,
IndexConfiguration: '' as Ref<Mixin<IndexingConfiguration<Doc>>>,
SpacesTypeData: '' as Ref<Mixin<Space>>
+24 -15
View File
@@ -46,9 +46,9 @@ import core from './component'
import { Hierarchy } from './hierarchy'
import { TxOperations } from './operations'
import { isPredicate } from './predicate'
import { Branding, BrandingMap } from './server'
import { DocumentQuery, FindResult } from './storage'
import { DOMAIN_TX } from './tx'
import { Branding, BrandingMap } from './server'
function toHex (value: number, chars: number): string {
const result = value.toString(16)
@@ -686,39 +686,48 @@ export function getFullTextIndexableAttributes (
return result
}
const ctxKey = 'indexer_ftc'
/**
* @public
*/
export function getFullTextContext (
hierarchy: Hierarchy,
objectClass: Ref<Class<Doc>>
objectClass: Ref<Class<Doc>>,
contexts: Map<Ref<Class<Doc>>, FullTextSearchContext>
): Omit<FullTextSearchContext, keyof Class<Doc>> {
let objClass = hierarchy.getClass(objectClass)
while (true) {
if (hierarchy.hasMixin(objClass, core.mixin.FullTextSearchContext)) {
const ctx = hierarchy.as<Class<Doc>, FullTextSearchContext>(objClass, core.mixin.FullTextSearchContext)
let ctx: Omit<FullTextSearchContext, keyof Class<Doc>> | undefined = hierarchy.getClassifierProp(objectClass, ctxKey)
if (ctx !== undefined) {
return ctx
}
if (typeof ctx !== 'string') {
const anc = hierarchy.getAncestors(objectClass)
for (const oc of anc) {
const ctx = contexts.get(oc)
if (ctx !== undefined) {
hierarchy.setClassifierProp(objectClass, ctxKey, ctx)
return ctx
}
}
if (objClass.extends === undefined) {
break
}
objClass = hierarchy.getClass(objClass.extends)
}
return {
ctx = {
toClass: objectClass,
fullTextSummary: false,
forceIndex: false,
propagate: [],
childProcessingAllowed: true
}
hierarchy.setClassifierProp(objectClass, ctxKey, ctx)
return ctx
}
/**
* @public
*/
export function isClassIndexable (hierarchy: Hierarchy, c: Ref<Class<Doc>>): boolean {
export function isClassIndexable (
hierarchy: Hierarchy,
c: Ref<Class<Doc>>,
contexts: Map<Ref<Class<Doc>>, FullTextSearchContext>
): boolean {
const indexed = hierarchy.getClassifierProp(c, 'class_indexed')
if (indexed !== undefined) {
return indexed as boolean
@@ -756,13 +765,13 @@ export function isClassIndexable (hierarchy: Hierarchy, c: Ref<Class<Doc>>): boo
let result = true
if (attrs.length === 0 && !(getFullTextContext(hierarchy, c)?.forceIndex ?? false)) {
if (attrs.length === 0 && !(getFullTextContext(hierarchy, c, contexts)?.forceIndex ?? false)) {
result = false
// We need check if document has collections with indexable fields.
const attrs = hierarchy.getAllAttributes(c).values()
for (const attr of attrs) {
if (attr.type._class === core.class.Collection) {
if (isClassIndexable(hierarchy, (attr.type as Collection<AttachedDoc>).of)) {
if (isClassIndexable(hierarchy, (attr.type as Collection<AttachedDoc>).of, contexts)) {
result = true
break
}
+38 -32
View File
@@ -3,10 +3,10 @@ import { type Asset, getMetadata, getResource } from '@hcengineering/platform'
import { getClient } from '.'
import notification from '@hcengineering/notification'
const sounds = new Map<Asset, AudioBufferSourceNode>()
const sounds = new Map<Asset, AudioBuffer>()
const context = new AudioContext()
export async function prepareSound (key: string, _class?: Ref<Class<Doc>>, loop = false, play = false): Promise<void> {
export async function prepareSound (key: string, _class?: Ref<Class<Doc>>): Promise<void> {
if (_class === undefined) return
const client = getClient()
@@ -23,39 +23,45 @@ export async function prepareSound (key: string, _class?: Ref<Class<Doc>>, loop
try {
const soundUrl = getMetadata(key as Asset) as string
const audioBuffer = await fetch(soundUrl)
.then(async (res) => await res.arrayBuffer())
.then(async (ArrayBuffer) => await context.decodeAudioData(ArrayBuffer))
const rawAudio = await fetch(soundUrl)
const rawBuffer = await rawAudio.arrayBuffer()
const decodedBuffer = await context.decodeAudioData(rawBuffer)
sounds.set(key as Asset, decodedBuffer)
} catch (err) {
console.error('Sound not found', key)
}
}
export async function playSound (
soundKey: string,
_class?: Ref<Class<Doc>>,
loop = false
): Promise<(() => void) | null> {
const soundAssetKey = soundKey as Asset
if (!sounds.has(soundAssetKey)) {
await prepareSound(soundKey, _class)
}
const sound = sounds.get(soundKey as Asset)
if (sound === undefined) {
console.error('Cannot prepare audio buffer', soundKey)
return null
}
try {
const audio = context.createBufferSource()
audio.buffer = audioBuffer
audio.buffer = sound
audio.loop = loop
sounds.set(key as Asset, audio)
if (play) {
playSound(key)
audio.connect(context.destination)
audio.start()
return (): void => {
audio.stop()
audio.disconnect(context.destination)
}
} catch (err) {
console.error('sound not found', key)
}
}
export function playSound (soundKey: string, _class?: Ref<Class<Doc>>, loop = false): void {
const sound = sounds.get(soundKey as Asset)
if (sound !== undefined) {
try {
sound.connect(context.destination)
sound.start()
} catch (err) {
console.error('error happened during sound play', soundKey, err)
}
} else {
void prepareSound(soundKey, _class, loop, true)
}
}
export function stopSound (soundKey: string): void {
const sound = sounds.get(soundKey as Asset)
if (sound !== undefined && sound?.context.state === 'running') {
sound.stop()
sound.disconnect(context.destination)
console.error('Error when playing sound back', soundKey, err)
return null
}
}
+8 -5
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { Markup } from '@hcengineering/core'
import { generateId, Markup } from '@hcengineering/core'
import { Extensions, getSchema } from '@tiptap/core'
import { Node, Schema } from '@tiptap/pm/model'
import { prosemirrorJSONToYDoc, prosemirrorToYDoc, yDocToProsemirrorJSON } from 'y-prosemirror'
@@ -50,7 +50,7 @@ export function jsonToYDocNoSchema (json: MarkupNode, field: string): YDoc {
const nodes = json.type === 'doc' ? json.content ?? [] : [json]
const content = nodes.map(nodeToYXmlElement)
const ydoc = new YDoc()
const ydoc = new YDoc({ guid: generateId() })
const fragment = ydoc.getXmlFragment(field)
fragment.push(content)
@@ -113,7 +113,10 @@ export function yDocContentToNodes (content: ArrayBuffer, schema?: Schema, exten
const nodes: Node[] = []
try {
const ydoc = new YDoc()
const ydoc = new YDoc({
gc: false,
guid: generateId()
})
const uint8arr = new Uint8Array(content)
applyUpdate(ydoc, uint8arr)
@@ -144,8 +147,8 @@ export function updateYDocContent (
schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions)
try {
const ydoc = new YDoc({ gc: false })
const res = new YDoc({ gc: false })
const ydoc = new YDoc({ guid: generateId(), gc: false })
const res = new YDoc({ guid: generateId(), gc: false })
const uint8arr = new Uint8Array(content)
applyUpdate(ydoc, uint8arr)
+1 -2
View File
@@ -44,7 +44,6 @@
export let disabled: boolean = false
export let isFold: boolean = false
export let isOpen: boolean = false
export let isSecondary: boolean = false
export let withBackground: boolean = false
export let showMenu: boolean = false
export let shouldTooltip: boolean = false
@@ -78,7 +77,7 @@
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
class="hulyNavItem-container line-height-auto {type} {type === 'type-anchor-link' || isSecondary
class="hulyNavItem-container line-height-auto {type} {type === 'type-anchor-link'
? 'font-regular-12'
: 'font-regular-14'}"
class:selected
@@ -123,7 +123,6 @@
_id={item.id}
icon={item.icon}
withIconBackground={item.withIconBackground}
isSecondary={item.isSecondary}
iconSize={item.iconSize}
{isSelected}
iconProps={{ ...item.iconProps, value: item.object }}
@@ -91,8 +91,7 @@
icon: icon ?? getObjectIcon(_class),
iconProps: { showStatus: true },
iconSize,
withIconBackground: !isDirect && !isPerson,
isSecondary: isDocChat && !isPerson
withIconBackground: !isDirect && !isPerson
})
}
@@ -71,7 +71,6 @@
{title}
{description}
selected={isSelected}
{isSecondary}
count={elementsCount > 0 ? elementsCount : null}
{type}
withBackground={withIconBackground}
@@ -46,7 +46,6 @@ export interface ChatNavItemModel {
icon: Asset | AnySvelteComponent | undefined
iconSize?: IconSize
iconProps: Record<string, any>
isSecondary: boolean
withIconBackground: boolean
}
+18 -7
View File
@@ -16,7 +16,8 @@ import presentation, {
loadServerConfig,
refreshClient,
setClient,
setPresentationCookie
setPresentationCookie,
upgradeDownloadProgress
} from '@hcengineering/presentation'
import {
desktopPlatform,
@@ -25,7 +26,8 @@ import {
navigate,
setMetadataLocalStorage
} from '@hcengineering/ui'
import { writable } from 'svelte/store'
import { writable, get } from 'svelte/store'
export const versionError = writable<string | undefined>(undefined)
const versionStorageKey = 'last_server_version'
@@ -82,20 +84,29 @@ export async function connect (title: string): Promise<Client | undefined> {
frontVersion !== serverVersion
) {
const reloaded = localStorage.getItem(`versionUpgrade:s${serverVersion}:f${frontVersion}`)
const isUpgrading = get(upgradeDownloadProgress) >= 0
if (reloaded === null) {
localStorage.setItem(`versionUpgrade:s${serverVersion}:f${frontVersion}`, 't')
location.reload()
// It might have been refreshed manually and download has started - do not reload
if (!isUpgrading) {
location.reload()
}
return false
} else {
versionError.set(`Front version ${frontVersion} is not in sync with server version ${serverVersion}`)
if (!desktopPlatform) {
if (!desktopPlatform || !isUpgrading) {
setTimeout(() => {
location.reload()
}, 5000)
// It might be possible that this callback will fire after the user has spent some time
// in the upgrade !modal! dialog and clicked upgrade - check again and do not reload
if (get(upgradeDownloadProgress) < 0) {
location.reload()
}
}, 10000)
}
// For embedded it should download the upgrade and restart the app
// For embedded if the download has started it should download the upgrade and restart the app
return false
}
@@ -57,6 +57,7 @@
export let ignoreInitialValidation: boolean = false
export let withProviders: boolean = false
export let subtitle: string | undefined = undefined
export let signUpDisabled = false
$: $themeStore.language && validate($themeStore.language)
@@ -138,7 +139,7 @@
}}
>
{#if loginState !== 'none'}
<Tabs {loginState} />
<Tabs {loginState} {signUpDisabled} />
{:else}
{#if subtitle !== undefined}
<div class="fs-title">
@@ -54,6 +54,7 @@
export let page: Pages = 'signup'
const signUpDisabled = getMetadata(login.metadata.DisableSignUp) ?? false
let navigateUrl: string | undefined
onDestroy(location.subscribe(updatePageLoc))
@@ -134,13 +135,13 @@
<Scroller padding={'1rem 0'}>
<div class="form-content">
{#if page === 'login'}
<LoginForm {navigateUrl} />
<LoginForm {navigateUrl} {signUpDisabled} />
{:else if page === 'signup'}
<SignupForm />
<SignupForm {signUpDisabled} />
{:else if page === 'createWorkspace'}
<CreateWorkspaceForm />
{:else if page === 'password'}
<PasswordRequest />
<PasswordRequest {signUpDisabled} />
{:else if page === 'recovery'}
<PasswordRestore />
{:else if page === 'selectWorkspace'}
@@ -21,6 +21,7 @@
import login from '../plugin'
export let navigateUrl: string | undefined = undefined
export let signUpDisabled = false
let method: LoginMethods = LoginMethods.Otp
@@ -44,12 +45,12 @@
</script>
{#if method === LoginMethods.Otp}
<LoginOtpForm {navigateUrl} on:change={changeMethod} />
<LoginOtpForm {navigateUrl} {signUpDisabled} on:change={changeMethod} />
<div class="action">
<BottomActionComponent action={loginWithPasswordAction} />
</div>
{:else}
<LoginPasswordForm {navigateUrl} on:change={changeMethod} />
<LoginPasswordForm {navigateUrl} {signUpDisabled} on:change={changeMethod} />
<div class="action">
<BottomActionComponent action={loginWithCodeAction} />
</div>
@@ -21,6 +21,7 @@
import { OtpLoginSteps, sendOtp } from '../index'
export let navigateUrl: string | undefined = undefined
export let signUpDisabled = false
const fields = [{ id: 'email', name: 'username', i18n: login.string.Email }]
const formData = {
@@ -57,11 +58,12 @@
{fields}
object={formData}
{action}
{signUpDisabled}
ignoreInitialValidation
withProviders
/>
{/if}
{#if step === OtpLoginSteps.Otp && formData.username !== ''}
<OtpForm email={formData.username} {navigateUrl} retryOn={otpRetryOn} on:step={handleStep} />
<OtpForm email={formData.username} {signUpDisabled} {navigateUrl} retryOn={otpRetryOn} on:step={handleStep} />
{/if}
@@ -21,6 +21,7 @@
import login from '../plugin'
export let navigateUrl: string | undefined = undefined
export let signUpDisabled = false
const fields = [
{ id: 'email', name: 'username', i18n: login.string.Email },
@@ -62,6 +63,7 @@
{fields}
{object}
{action}
{signUpDisabled}
bottomActions={[recoveryAction]}
ignoreInitialValidation
withProviders
@@ -28,6 +28,7 @@
export let navigateUrl: string | undefined = undefined
export let email: string
export let retryOn: Timestamp
export let signUpDisabled = false
const dispatch = createEventDispatcher()
@@ -232,7 +233,7 @@
style:min-height={$deviceInfo.docHeight > 720 ? '42rem' : '0'}
>
<div class="header">
<Tabs loginState="login" />
<Tabs loginState="login" {signUpDisabled} />
<div class="description">
<Label label={login.string.SentTo} />
<span class="email ml-1">
@@ -23,8 +23,9 @@
import { BottomAction } from '..'
import { signUpAction } from '../actions'
const fields = [{ id: 'email', name: 'username', i18n: login.string.Email }]
export let signUpDisabled = false
const fields = [{ id: 'email', name: 'username', i18n: login.string.Email }]
const object = {
username: ''
}
@@ -64,7 +65,7 @@
goTo('login')
}
},
signUpAction
...(signUpDisabled ? [] : [signUpAction])
]
</script>
@@ -21,6 +21,8 @@
import { goTo, signUp } from '../utils'
import Form from './Form.svelte'
export let signUpDisabled = false
const fields = [
{ id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true },
{ id: 'family-name', name: 'last', i18n: login.string.LastName, short: true },
@@ -39,6 +41,10 @@
let status: Status<any> = OK
if (signUpDisabled) {
goTo('login')
}
const action = {
i18n: login.string.SignUp,
func: async () => {
@@ -19,6 +19,7 @@
import login from '../plugin'
export let loginState: 'login' | 'signup' | 'none' = 'none'
export let signUpDisabled = false
const goTab = (path: string) => {
const loc = getCurrentLocation()
@@ -29,16 +30,18 @@
</script>
<div class="flex-row-center caption">
<a
class="title"
class:selected={loginState === 'signup'}
href="."
on:click|preventDefault={() => {
if (loginState !== 'signup') goTab('signup')
}}
>
<Label label={login.string.SignUp} />
</a>
{#if !signUpDisabled}
<a
class="title"
class:selected={loginState === 'signup'}
href="."
on:click|preventDefault={() => {
if (loginState !== 'signup') goTab('signup')
}}
>
<Label label={login.string.SignUp} />
</a>
{/if}
<a
class="title"
class:selected={loginState === 'login'}
+2 -1
View File
@@ -72,7 +72,8 @@ export default plugin(loginId, {
LoginTokens: '' as Metadata<Record<string, string>>,
LastToken: '' as Metadata<string>,
LoginEndpoint: '' as Metadata<string>,
LoginEmail: '' as Metadata<string>
LoginEmail: '' as Metadata<string>,
DisableSignUp: '' as Metadata<boolean>
},
component: {
LoginApp: '' as AnyComponent,
@@ -16,7 +16,7 @@
import { PersonAccount, formatName } from '@hcengineering/contact'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { getCurrentAccount } from '@hcengineering/core'
import { getClient, playSound, stopSound } from '@hcengineering/presentation'
import { getClient, playSound } from '@hcengineering/presentation'
import { Button, Label } from '@hcengineering/ui'
import { JoinRequest, RequestStatus } from '@hcengineering/love'
import love from '../plugin'
@@ -29,6 +29,7 @@
$: person = $personByIdStore.get(request.person)
const client = getClient()
let stopSound: (() => void) | null = null
async function accept (): Promise<void> {
await client.update(request, { status: RequestStatus.Approved })
@@ -43,11 +44,13 @@
async function decline (): Promise<void> {
await client.update(request, { status: RequestStatus.Rejected })
}
onMount(() => {
playSound(love.sound.Knock, love.class.JoinRequest, true)
onMount(async () => {
stopSound = await playSound(love.sound.Knock, love.class.JoinRequest, true)
})
onDestroy(() => {
stopSound(love.sound.Knock)
stopSound?.()
})
</script>
+1 -1
View File
@@ -21,7 +21,7 @@ import { SupportClientFactory, SupportConversation, SupportSystem } from './type
export * from './types'
export { deleteSupportConversation, updateSupportConversation } from './utils'
export const supportLink = 'https://join.slack.com/t/hulycommunity/shared_invite/zt-2cyrevz8g-AGqEDZNsujbn4wHOWd7myg'
export const supportLink = 'https://huly.link/slack'
export const reportBugLink = 'https://github.com/hcengineering/platform/issues/new'
export const docsLink = 'http://docs.huly.io/'
export const privacyPolicyLink = 'https://v1.huly.io/legal/privacy/'
@@ -183,6 +183,14 @@
d="M18.2927 19.7071C17.9022 19.3166 17.9022 18.6834 18.2927 18.2929C18.6833 17.9024 19.3164 17.9024 19.707 18.2929L26 24.586V20H28V28H20V26H24.586L18.2927 19.7071Z"
/>
</symbol>
<symbol id="download" viewBox="0 0 24 24">
<path
d="M18.7,9.6C18.5,6,15.6,3.2,12,3.2C8.4,3.2,5.5,6,5.3,9.5c-2.3,0.7-4,2.9-4,5.5c0,3.2,2.6,5.8,5.8,5.8 c0.5,0,1.1-0.1,1.6-0.2C9,20.4,9.2,20,9.1,19.6S8.6,19,8.2,19.1c-0.4,0.1-0.8,0.2-1.2,0.2c-2.3,0-4.2-1.9-4.2-4.2 c0-2,1.4-3.7,3.3-4.1c0.2,0,0.3-0.1,0.4-0.2c0.2-0.1,0.4-0.4,0.4-0.6c0-2.9,2.4-5.2,5.2-5.2s5.2,2.4,5.2,5.2c0,0.1,0,0.1,0,0.2 c0,0.3,0.2,0.6,0.6,0.7c1.9,0.4,3.3,2.1,3.3,4.1c0,2.3-1.9,4.2-4.2,4.2c-0.4,0-0.8-0.1-1.2-0.2c-0.4-0.1-0.8,0.1-0.9,0.5 c-0.1,0.4,0.1,0.8,0.5,0.9c0.5,0.1,1,0.2,1.6,0.2c3.2,0,5.8-2.6,5.8-5.8C22.6,12.5,21,10.4,18.7,9.6z"
/>
<path
d="M16.1,14.5c-0.3-0.3-0.8-0.3-1.1,0l-2.3,2.3V10c0-0.4-0.3-0.8-0.8-0.8s-0.8,0.3-0.8,0.8v6.7l-2.2-2.2 c-0.3-0.3-0.8-0.3-1.1,0c-0.3,0.3-0.3,0.8,0,1.1l3.5,3.5c0.3,0.3,0.8,0.3,1.1,0l3.6-3.5C16.4,15.2,16.4,14.8,16.1,14.5z"
/>
</symbol>
<symbol id="note" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none"></rect><line x1="96" x2="160" y1="96" y2="96" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"></line><line x1="96" x2="160" y1="128" y2="128" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"></line><line x1="96" x2="128" y1="160" y2="160" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"></line><path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8" d="M156.68629,216H48a8,8,0,0,1-8-8V48a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8V156.68629a8,8,0,0,1-2.34315,5.65686l-51.3137,51.3137A8,8,0,0,1,156.68629,216Z"></path><polyline fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8" points="215.277 159.992 160 159.992 160 215.272"></polyline>
</symbol>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

+1
View File
@@ -32,6 +32,7 @@
"AlignCenter": "Align center",
"AlignLeft": "Align left",
"AlignRight": "Align right",
"Download": "Download",
"ViewImage": "View image",
"ViewOriginal": "View original",
"MoreActions": "More actions",
+1
View File
@@ -32,6 +32,7 @@
"AlignCenter": "Alinear centro",
"AlignLeft": "Alinear izquierda",
"AlignRight": "Alinear derecha",
"Download": "Descargar",
"ViewImage": "Ver imagen",
"ViewOriginal": "Ver original",
"MoreActions": "Más acciones",
+1
View File
@@ -32,6 +32,7 @@
"AlignCenter": "Aligner au centre",
"AlignLeft": "Aligner à gauche",
"AlignRight": "Aligner à droite",
"Download": "Télécharger",
"ViewImage": "Voir l'image",
"ViewOriginal": "Voir l'original",
"MoreActions": "Plus d'actions",
+1
View File
@@ -32,6 +32,7 @@
"AlignCenter": "Alinhar centro",
"AlignLeft": "Alinhar esquerda",
"AlignRight": "Alinhar direita",
"Download": "Transferir",
"ViewImage": "Ver imagem",
"ViewOriginal": "Ver original",
"MoreActions": "Mais ações",
+1
View File
@@ -32,6 +32,7 @@
"AlignCenter": "По центру",
"AlignLeft": "По левому краю",
"AlignRight": "По правому краю",
"Download": "Скачать",
"ViewImage": "Открыть изображение",
"ViewOriginal": "Открыть оригинал",
"MoreActions": "Дополнительные действия",
+1
View File
@@ -32,6 +32,7 @@
"AlignCenter": "居中对齐",
"AlignLeft": "左对齐",
"AlignRight": "右对齐",
"Download": "下载",
"ViewImage": "查看图片",
"ViewOriginal": "查看原图",
"MoreActions": "更多操作",
+1
View File
@@ -38,5 +38,6 @@ loadMetadata(textEditor.icon, {
MoreH: `${icons}#moreH`,
Expand: `${icons}#expand`,
ScaleOut: `${icons}#scaleOut`,
Download: `${icons}#download`,
Note: `${icons}#note`
})
@@ -16,7 +16,7 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { type Space, type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
import { type Space, type Class, type CollaborativeDoc, type Doc, type Ref, generateId } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { getFileUrl, getImageSize, imageSizeToRatio } from '@hcengineering/presentation'
import { markupToJSON } from '@hcengineering/text'
@@ -102,7 +102,7 @@
const dispatch = createEventDispatcher()
const ydoc = getContext<YDoc>(CollaborationIds.Doc) ?? new YDoc()
const ydoc = getContext<YDoc>(CollaborationIds.Doc) ?? new YDoc({ guid: generateId() })
const contextProvider = getContext<Provider>(CollaborationIds.Provider)
const localProvider = createLocalProvider(ydoc, collaborativeDoc)
@@ -233,6 +233,19 @@ export async function openImage (editor: Editor): Promise<void> {
})
}
export async function downloadImage (editor: Editor): Promise<void> {
const attributes = editor.getAttributes('image')
const fileId = attributes['file-id'] ?? attributes.src
const href = getFileUrl(fileId)
const link = document.createElement('a')
link.style.display = 'none'
link.target = '_blank'
link.href = href
link.download = attributes.title ?? attributes.alt ?? ''
link.click()
}
export async function expandImage (editor: Editor): Promise<void> {
const attributes = editor.getAttributes('image')
const fileId = attributes['file-id'] ?? attributes.src
+2 -1
View File
@@ -18,7 +18,7 @@ import { type Resources } from '@hcengineering/platform'
import { formatLink } from './kits/default-kit'
import { isEditable, isHeadingVisible } from './kits/editor-kit'
import { openTableOptions, isEditableTableActive } from './components/extension/table/table'
import { openImage, expandImage, moreImageActions } from './components/extension/imageExt'
import { openImage, downloadImage, expandImage, moreImageActions } from './components/extension/imageExt'
import { configureNote, isEditableNote } from './components/extension/note'
export * from '@hcengineering/presentation/src/types'
@@ -82,6 +82,7 @@ export default async (): Promise<Resources> => ({
OpenTableOptions: openTableOptions,
OpenImage: openImage,
ExpandImage: expandImage,
DownloadImage: downloadImage,
MoreImageActions: moreImageActions,
ConfigureNote: configureNote,
IsEditableTableActive: isEditableTableActive,
@@ -13,7 +13,7 @@
// limitations under the License.
//
import { type Ref, type CollaborativeDoc, type Doc, type Class } from '@hcengineering/core'
import { type Ref, type CollaborativeDoc, type Doc, type Class, generateId } from '@hcengineering/core'
import {
type DocumentId,
type PlatformDocumentId,
@@ -99,7 +99,7 @@ export const createTiptapCollaborationData = (params: {
objectId?: Ref<Doc>
objectAttr?: string
}): { provider: Provider, ydoc: Ydoc } => {
const ydoc: Ydoc = new Ydoc()
const ydoc: Ydoc = new Ydoc({ guid: generateId() })
return {
ydoc,
provider: createRemoteProvider(ydoc, params)
+2
View File
@@ -72,6 +72,7 @@ export default plugin(textEditorId, {
AlignRight: '' as IntlString,
ViewImage: '' as IntlString,
ViewOriginal: '' as IntlString,
Download: '' as IntlString,
MoreActions: '' as IntlString,
InsertTable: '' as IntlString,
@@ -115,6 +116,7 @@ export default plugin(textEditorId, {
MoreH: '' as Asset,
Expand: '' as Asset,
ScaleOut: '' as Asset,
Download: '' as Asset,
Note: '' as Asset
}
})
+1 -1
View File
@@ -24,7 +24,7 @@
"OpenInNewTab": "Open in a new tab",
"NewVersionAvailable": "New version is available",
"PleaseUpdate": "Please update",
"ServerUnderMaintenance": "Server is under maintenance",
"ServerUnderMaintenance": "Preparing workspace for new version...",
"MobileNotSupported": "Sorry, mobile devices support coming soon. In the meantime, please use Desktop",
"LogInAnyway": "Log in anyway",
"WorkspaceCreating": "Creation in progress...",
+1 -1
View File
@@ -24,7 +24,7 @@
"OpenInNewTab": "Abrir en una nueva pestaña",
"NewVersionAvailable": "Nueva versión disponible",
"PleaseUpdate": "Por favor, actualice",
"ServerUnderMaintenance": "El servidor está en mantenimiento",
"ServerUnderMaintenance": "Preparando el espacio de trabajo para la nueva versión...",
"MobileNotSupported": "Disculpa, el soporte para dispositivos móviles estará disponible próximamente. Mientras tanto, por favor usa el escritorio.",
"LogInAnyway": "Iniciar sesión de todas formas",
"WorkspaceCreating": "Creation in progress...",
+1 -1
View File
@@ -24,7 +24,7 @@
"OpenInNewTab": "Ouvrir dans un nouvel onglet",
"NewVersionAvailable": "Nouvelle version disponible",
"PleaseUpdate": "Veuillez mettre à jour",
"ServerUnderMaintenance": "Le serveur est en maintenance",
"ServerUnderMaintenance": "Préparation de l'espace de travail pour la nouvelle version...",
"MobileNotSupported": "Désolé, le support pour les appareils mobiles arrive bientôt. En attendant, veuillez utiliser un ordinateur de bureau",
"LogInAnyway": "Se connecter quand même",
"WorkspaceCreating": "Création en cours...",
+1 -1
View File
@@ -24,7 +24,7 @@
"OpenInNewTab": "Abrir num novo separador",
"NewVersionAvailable": "Nova versão disponível",
"PleaseUpdate": "Atualize",
"ServerUnderMaintenance": "Servidor em manutenção",
"ServerUnderMaintenance": "Preparando o espaço de trabalho para a nova versão...",
"MobileNotSupported": "Desculpe, o suporte para dispositivos móveis estará disponível em breve. Enquanto isso, por favor, use o Desktop.",
"LogInAnyway": "Entrar de qualquer maneira",
"WorkspaceCreating": "Creation in progress...",
+1 -1
View File
@@ -24,7 +24,7 @@
"OpenInNewTab": "Открыть в новой вкладке",
"NewVersionAvailable": "Доступна новая версия",
"PleaseUpdate": "Пожалуйста, обновите приложение",
"ServerUnderMaintenance": "Обслуживание сервера",
"ServerUnderMaintenance": "Подготовка рабочего пространства для новой версии...",
"MobileNotSupported": "Простите, поддержка мобильных устройств скоро будет доступна. Пока воспользуйтесь компьютером.",
"LogInAnyway": "Все равно войти",
"WorkspaceCreating": "Пространство создается...",
+1 -1
View File
@@ -24,7 +24,7 @@
"OpenInNewTab": "在新标签页中打开",
"NewVersionAvailable": "有新版本可用",
"PleaseUpdate": "请更新",
"ServerUnderMaintenance": "服务器正在维护中",
"ServerUnderMaintenance": "正在为新版本准备工作区...",
"MobileNotSupported": "抱歉,移动设备支持即将推出。在此期间,请使用桌面设备",
"LogInAnyway": "仍然登录",
"WorkspaceCreating": "创建进行中...",
@@ -221,6 +221,7 @@
selectTab(tabToReplace._id)
prevTabIdStore.set(tabToReplace._id)
} else {
console.log('Creating new tab on init')
const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
attachedTo: account._id,
location: url,
@@ -23,7 +23,7 @@
languageStore,
locationToUrl
} from '@hcengineering/ui'
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
import { ComponentExtensions, getClient, reduceCalls } from '@hcengineering/presentation'
import { Asset, getResource, translate } from '@hcengineering/platform'
import { WorkbenchTab } from '@hcengineering/workbench'
import view from '@hcengineering/view'
@@ -40,8 +40,18 @@
let icon: Asset | AnySvelteComponent | undefined
let iconProps: Record<string, any> | undefined
let lastLocUrl: string | undefined = undefined
let lastLang: string | undefined = undefined
async function updateTabData (tab: WorkbenchTab, lang: string): Promise<void> {
const tabLoc = $tabIdStore === tab._id ? getCurrentLocation() : getTabLocation(tab)
const url = locationToUrl(tabLoc)
if (lastLocUrl === url && lastLang === lang) return
lastLocUrl = url
lastLang = lang
const data = await getTabDataByLocation(tabLoc)
name = data.name ?? (await translate(data.label, {}, lang))
@@ -53,11 +63,15 @@
iconProps = data.iconProps
if (tab.name !== name && tab.location === locationToUrl(tabLoc)) {
await client.update(tab, { name })
await client.diffUpdate(tab, { name })
}
}
$: void updateTabData(tab, $languageStore)
const update = reduceCalls(async function (tab: WorkbenchTab, lang: string): Promise<void> {
await updateTabData(tab, lang)
})
$: void update(tab, $languageStore)
function handleClickTab (): void {
selectTab(tab._id)
+18 -7
View File
@@ -24,7 +24,8 @@ import presentation, {
refreshClient,
setClient,
setPresentationCookie,
uiContext
uiContext,
upgradeDownloadProgress
} from '@hcengineering/presentation'
import {
desktopPlatform,
@@ -34,7 +35,8 @@ import {
navigate,
setMetadataLocalStorage
} from '@hcengineering/ui'
import { writable } from 'svelte/store'
import { writable, get } from 'svelte/store'
import plugin from './plugin'
import { workspaceCreating } from './utils'
@@ -164,20 +166,29 @@ export async function connect (title: string): Promise<Client | undefined> {
frontVersion !== serverVersion
) {
const reloaded = localStorage.getItem(`versionUpgrade:s${serverVersion}:f${frontVersion}`)
const isUpgrading = get(upgradeDownloadProgress) >= 0
if (reloaded === null) {
localStorage.setItem(`versionUpgrade:s${serverVersion}:f${frontVersion}`, 't')
location.reload()
// It might have been refreshed manually and download has started - do not reload
if (!isUpgrading) {
location.reload()
}
return false
} else {
versionError.set(`Front version ${frontVersion} is not in sync with server version ${serverVersion}`)
if (!desktopPlatform) {
if (!desktopPlatform || !isUpgrading) {
setTimeout(() => {
location.reload()
}, 5000)
// It might be possible that this callback will fire after the user has spent some time
// in the upgrade !modal! dialog and clicked upgrade - check again and do not reload
if (get(upgradeDownloadProgress) < 0) {
location.reload()
}
}, 10000)
}
// For embedded it should download the upgrade and restart the app
// For embedded if the download has started it should download the upgrade and restart the app
return false
}
+15 -16
View File
@@ -19,8 +19,8 @@ import core, {
type Doc,
getCurrentAccount,
type Ref,
RateLimiter,
generateId
generateId,
reduceCalls
} from '@hcengineering/core'
import { type Application, workbenchId, type WorkbenchTab } from '@hcengineering/workbench'
import {
@@ -56,20 +56,13 @@ tabIdStore.subscribe((value) => {
prevTabId = value
})
// Use rate limiter to control tab creation, preventing multiple tabs during fast location changing
const limiter = new RateLimiter(1)
workspaceStore.subscribe((workspace) => {
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
})
locationStore.subscribe((l: Location) => {
void limiter.add(syncTabLoc)
})
tabIdStore.subscribe(saveTabToLocalStorage)
async function syncTabLoc (): Promise<void> {
const syncTabLoc = reduceCalls(async (): Promise<void> => {
const loc = getCurrentLocation()
const workspace = get(workspaceStore)
if (workspace == null || workspace === '') return
@@ -102,7 +95,7 @@ async function syncTabLoc (): Promise<void> {
}
const me = getCurrentAccount()
const tab: WorkbenchTab = {
const newTab: WorkbenchTab = {
_id: generateId(),
_class: workbench.class.WorkbenchTab,
space: core.space.Workspace,
@@ -113,9 +106,10 @@ async function syncTabLoc (): Promise<void> {
modifiedOn: Date.now(),
modifiedBy: me._id
}
await getClient().createDoc(workbench.class.WorkbenchTab, core.space.Workspace, tab, tab._id)
tabsStore.update((tabs) => [...tabs, tab])
selectTab(tab._id)
console.log('Creating new tab when pinned location changed', { newLocation: url, pinnedLocation: tab.location })
await getClient().createDoc(workbench.class.WorkbenchTab, core.space.Workspace, newTab, newTab._id)
tabsStore.update((tabs) => [...tabs, newTab])
selectTab(newTab._id)
} else {
// TODO: Fix this
// if (
@@ -127,9 +121,14 @@ async function syncTabLoc (): Promise<void> {
// return
// }
await getClient().update(tab, { location: url, name })
await getClient().diffUpdate(tab, { location: url, name })
}
}
})
locationStore.subscribe((l: Location) => {
void syncTabLoc()
})
export function syncWorkbenchTab (): void {
const workspace = get(workspaceStore)
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
+29 -10
View File
@@ -13,7 +13,8 @@ export function registerGithub (
accountsUrl: string,
dbPromise: Promise<AccountDB>,
frontUrl: string,
brandings: BrandingMap
brandings: BrandingMap,
signUpDisabled?: boolean
): string | undefined {
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET
@@ -69,7 +70,7 @@ export function registerGithub (
const [first, last] = ctx.state.user.displayName?.split(' ') ?? [ctx.state.user.username, '']
measureCtx.info('Provider auth handler', { email, type: 'github' })
if (email !== undefined) {
let loginInfo: LoginInfo
let loginInfo: LoginInfo | null
const state = safeParseAuthState(ctx.query?.state)
const branding = getBranding(brandings, state?.branding)
const db = await dbPromise
@@ -78,17 +79,35 @@ export function registerGithub (
githubId: ctx.state.user.id
})
} else {
loginInfo = await loginWithProvider(measureCtx, db, null, email, first, last, {
githubId: ctx.state.user.id
})
loginInfo = await loginWithProvider(
measureCtx,
db,
null,
email,
first,
last,
{
githubId: ctx.state.user.id
},
signUpDisabled
)
}
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
if (loginInfo === null) {
measureCtx.info('Failed to auth: no associated account found', {
email,
type: 'github',
user: ctx.state?.user
})
ctx.redirect(concatLink(branding?.front ?? frontUrl, '/login'))
} else {
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
measureCtx.info('Success auth, redirect', { email, type: 'github', target: origin })
// Successful authentication, redirect to your application
ctx.redirect(`${origin}?${query}`)
measureCtx.info('Success auth, redirect', { email, type: 'github', target: origin })
// Successful authentication, redirect to your application
ctx.redirect(`${origin}?${query}`)
}
}
} catch (err: any) {
measureCtx.error('failed to auth', { err, type: 'github', user: ctx.state?.user })
+18 -8
View File
@@ -13,7 +13,8 @@ export function registerGoogle (
accountsUrl: string,
dbPromise: Promise<AccountDB>,
frontUrl: string,
brandings: BrandingMap
brandings: BrandingMap,
signUpDisabled?: boolean
): string | undefined {
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET
@@ -70,22 +71,31 @@ export function registerGoogle (
measureCtx.info('Provider auth handler', { email, type: 'google' })
if (email !== undefined) {
try {
let loginInfo: LoginInfo
let loginInfo: LoginInfo | null
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 {
loginInfo = await loginWithProvider(measureCtx, db, null, email, first, last)
loginInfo = await loginWithProvider(measureCtx, db, null, email, first, last, undefined, signUpDisabled)
}
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
if (loginInfo === null) {
measureCtx.info('Failed to auth: no associated account found', {
email,
type: 'google',
user: ctx.state?.user
})
ctx.redirect(concatLink(branding?.front ?? frontUrl, '/login'))
} else {
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
// Successful authentication, redirect to your application
measureCtx.info('Success auth, redirect', { email, type: 'google', target: origin })
ctx.redirect(`${origin}?${query}`)
// Successful authentication, redirect to your application
measureCtx.info('Success auth, redirect', { email, type: 'google', target: origin })
ctx.redirect(`${origin}?${query}`)
}
} catch (err: any) {
measureCtx.error('failed to auth', { err, type: 'google', user: ctx.state?.user })
}
+5 -3
View File
@@ -18,7 +18,8 @@ export type AuthProvider = (
accountsUrl: string,
db: Promise<AccountDB>,
frontUrl: string,
brandings: BrandingMap
brandings: BrandingMap,
signUpDisabled?: boolean
) => string | undefined
export function registerProviders (
@@ -28,7 +29,8 @@ export function registerProviders (
db: Promise<AccountDB>,
serverSecret: string,
frontUrl: string | undefined,
brandings: BrandingMap
brandings: BrandingMap,
signUpDisabled: boolean = false
): void {
const accountsUrl = process.env.ACCOUNTS_URL
if (accountsUrl === undefined) {
@@ -63,7 +65,7 @@ export function registerProviders (
const res: string[] = []
const providers: AuthProvider[] = [registerGoogle, registerGithub, registerOpenid]
for (const provider of providers) {
const value = provider(ctx, passport, router, accountsUrl, db, frontUrl, brandings)
const value = provider(ctx, passport, router, accountsUrl, db, frontUrl, brandings, signUpDisabled)
if (value !== undefined) res.push(value)
}
+29 -10
View File
@@ -28,7 +28,8 @@ export function registerOpenid (
accountsUrl: string,
dbPromise: Promise<AccountDB>,
frontUrl: string,
brandings: BrandingMap
brandings: BrandingMap,
signUpDisabled?: boolean
): string | undefined {
const openidClientId = process.env.OPENID_CLIENT_ID
const openidClientSecret = process.env.OPENID_CLIENT_SECRET
@@ -90,7 +91,7 @@ export function registerOpenid (
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
let loginInfo: LoginInfo | null
const state = safeParseAuthState(ctx.query?.state)
const branding = getBranding(brandings, state?.branding)
const db = await dbPromise
@@ -99,17 +100,35 @@ export function registerOpenid (
openId: ctx.state.user.sub
})
} else {
loginInfo = await loginWithProvider(measureCtx, db, null, email, first, last, {
openId: ctx.state.user.sub
})
loginInfo = await loginWithProvider(
measureCtx,
db,
null,
email,
first,
last,
{
openId: ctx.state.user.sub
},
signUpDisabled
)
}
const origin = concatLink(branding?.front ?? frontUrl, '/login/auth')
const query = encodeURIComponent(qs.stringify({ token: loginInfo.token }))
if (loginInfo === null) {
measureCtx.info('Failed to auth: no associated account found', {
email,
type: 'openid',
user: ctx.state?.user
})
ctx.redirect(concatLink(branding?.front ?? frontUrl, '/login'))
} else {
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}`)
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 })
+2 -1
View File
@@ -13,7 +13,8 @@ export function registerToken (
accountsUrl: string,
dbPromise: Promise<AccountDB>,
frontUrl: string,
brandings: BrandingMap
brandings: BrandingMap,
signUpDisabled?: boolean
): string | undefined {
passport.use(
'token',
+1 -1
View File
@@ -8,7 +8,7 @@
"template": "@hcengineering/node-package",
"license": "EPL-2.0",
"scripts": {
"start": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret OPERATION_PROFILING=false MODEL_JSON=../../models/all/bundle/model.json node bundle/bundle.js",
"start": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret OPERATION_PROFILING=false MODEL_JSON=../../models/all/bundle/model.json node --inspect bundle/bundle.js",
"start-u": "rush bundle --to @hcengineering/pod-server && ./bundle/ && cross-env NODE_ENV=production SERVER_PROVIDER=uweb ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret MODEL_JSON=../../models/all/bundle/model.json node bundle/bundle.js",
"start-flame": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production ELASTIC_INDEX_NAME=local_storage_index MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret MODEL_JSON=../../models/all/bundle/model.json clinic flame --dest ./out -- node --nolazy -r ts-node/register --enable-source-maps src/__start.ts",
"build": "compile",
+3
View File
@@ -14,6 +14,9 @@
# Create third user record in accounts
./tool-local.sh create-account user3 -f Cain -l Velasquez -p 1234
./tool-local.sh confirm-email user3
# Create fourth user record in accounts
./tool-local.sh create-account user4 -f Armin -l Karmin -p 1234
./tool-local.sh confirm-email user4
./tool-local.sh create-account user_qara -f Qara -l Admin -p 1234
./tool-local.sh confirm-email user_qara
+3 -1
View File
@@ -28,14 +28,16 @@ fi
./tool.sh create-account user1 -f John -l Appleseed -p 1234
./tool.sh create-account user2 -f Kainin -l Dirak -p 1234
./tool.sh create-account user3 -f Cain -l Velasquez -p 1234
./tool.sh create-account user4 -f Armin -l Karmin -p 1234
./tool.sh assign-workspace user1 sanity-ws-qms
./tool.sh assign-workspace user2 sanity-ws-qms
./tool.sh assign-workspace user3 sanity-ws-qms
./tool.sh assign-workspace user4 sanity-ws-qms
# Make user the workspace maintainer
./tool.sh confirm-email user1
./tool.sh confirm-email user2
./tool.sh confirm-email user3
./tool.sh confirm-email user4
./tool.sh create-account user_qara -f Qara -l Admin -p 1234
./tool.sh assign-workspace user_qara sanity-ws-qms
+1
View File
@@ -15,6 +15,7 @@ export SERVER_SECRET=secret
./tool-local.sh assign-workspace user1 sanity-ws-qms
./tool-local.sh assign-workspace user2 sanity-ws-qms
./tool-local.sh assign-workspace user3 sanity-ws-qms
./tool-local.sh assign-workspace user4 sanity-ws-qms
./tool-local.sh assign-workspace user_qara sanity-ws-qms
./tool-local.sh set-user-role user1 sanity-ws-qms OWNER
+1
View File
@@ -9,6 +9,7 @@
./tool.sh assign-workspace user1 sanity-ws-qms
./tool.sh assign-workspace user2 sanity-ws-qms
./tool.sh assign-workspace user3 sanity-ws-qms
./tool.sh assign-workspace user4 sanity-ws-qms
./tool.sh set-user-role user2 sanity-ws-qms OWNER
./tool.sh assign-workspace user_qara sanity-ws-qms
+4 -3
View File
@@ -19,9 +19,10 @@
"format": "format tests",
"ci": "playwright install --with-deps chromium",
"test": "",
"uitest": "cross-env LOCAL_URL=http://host.docker.internal:3003/ DEV_URL= playwright test -c ./tests/playwright.config.ts",
"staging-uitest": "cross-env PLATFORM_URI=https://front.hc.engineering/ playwright test -c ./tests/playwright.config.ts --grep @staging",
"dev-uitest": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://host.docker.internal:3333 SETTING=storage-dev.json SETTING_SECOND=storageSecond-dev.json DEV_URL=http://localhost:8080/account playwright test -c ./tests/playwright.config.ts",
"uitest": "cross-env LOCAL_URL=http://host.docker.internal:3003/ DEV_URL= playwright test -c ./tests/playwright.config.ts --grep-invert @PDF",
"uitest-pdf": "cross-env LOCAL_URL=http://host.docker.internal:3003/ DEV_URL= playwright test -c ./tests/playwright.config.ts --grep @PDF",
"staging-uitest": "cross-env PLATFORM_URI=https://front.hc.engineering/ playwright test -c ./tests/playwright.config.ts --grep @staging --grep-invert @PDF",
"dev-uitest": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://host.docker.internal:3333 SETTING=storage-dev.json SETTING_SECOND=storageSecond-dev.json DEV_URL=http://localhost:8080/account playwright test -c ./tests/playwright.config.ts --grep-invert @PDF",
"debug": "playwright test -c ./tests/playwright.config.ts --debug --headed",
"dev-debug": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://host.docker.internal:3333 SETTING=storage-dev.json SETTING_SECOND=storageSecond-dev.json playwright test -c ./tests/playwright.config.ts --debug --headed",
"codegen": "playwright codegen --load-storage storage.json http://host.docker.internal:8083/workbench/sanity-ws/",
+38
View File
@@ -0,0 +1,38 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "login:metadata:LoginEmail",
"value": "user4"
},
{
"name": "login:metadata:LoginTokens",
"value": "{\"sanity-ws-qms\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb25maXJtZWQiOnRydWUsImVtYWlsIjoidXNlcjQiLCJ3b3Jrc3BhY2UiOiIifQ.ww1aIf8yrNtS2a7nlLuq0EF2zKN04ThDHp5nSMwhj80\"}"
},
{
"name": "login:metadata:LoginEndpoint",
"value": "ws://localhost:3334"
},
{
"name": "#platform.notification.logging",
"value": "false"
},
{
"name": "#platform.lazy.loading",
"value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
},
{
"name": "#platform.notification.timeout",
"value": "0"
}
]
}
]
}
+38
View File
@@ -0,0 +1,38 @@
{
"cookies": [],
"origins": [
{
"origin": "http://host.docker.internal:8083",
"localStorage": [
{
"name": "login:metadata:LoginEmail",
"value": "user4"
},
{
"name": "login:metadata:LoginTokens",
"value": "{\"sanity-ws-qms\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjb25maXJtZWQiOnRydWUsImVtYWlsIjoidXNlcjQiLCJ3b3Jrc3BhY2UiOiIifQ.ww1aIf8yrNtS2a7nlLuq0EF2zKN04ThDHp5nSMwhj80\"}"
},
{
"name": "login:metadata:LoginEndpoint",
"value": "ws://host.docker.internal:3334"
},
{
"name": "#platform.notification.logging",
"value": "false"
},
{
"name": "#platform.lazy.loading",
"value": "false"
},
{
"name": "flagOpenInDesktopApp",
"value": "true"
},
{
"name": "#platform.notification.timeout",
"value": "0"
}
]
}
]
}
@@ -0,0 +1,101 @@
import { devices, test } from '@playwright/test'
import { attachScreenshot, HomepageURI, PlatformSetting, PlatformURI, waitForNetworIdle } from '../utils'
import { allure } from 'allure-playwright'
import { DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../model/types'
import { DocumentContentPage } from '../model/documents/document-content-page'
import { prepareDocumentStep } from './common-documents-steps'
import { DocumentApprovalsPage } from '../model/documents/document-approvals-page'
import { PdfPages } from '../model/documents/pdf-pages'
import { VisualCheck } from '../model/visual-check'
test.use({
storageState: PlatformSetting,
...devices['Desktop Edge'],
channel: 'msedge'
})
test.describe('@PDF. QMS. PDF Download and Preview', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished()
})
test.afterEach(async ({ browser }) => {
const contexts = browser.contexts()
for (const context of contexts) {
await context.close()
}
})
test('TESTS-277 - @PDF Author can Generate PDF from a doc with all Reviewers and Approvers signature info displayed', async ({
page
}) => {
await allure.description('Requirement\nUsers need to review the document and review status is displayed in PDF')
await allure.tms('TESTS-277', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-277')
const approveDocument: NewDocument = {
template: 'HR (HR)',
title: 'Approve document-Test',
description: 'Approve document description-Test'
}
const documentDetails: DocumentDetails = {
type: 'HR',
category: 'Human Resources',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
}
await prepareDocumentStep(page, approveDocument)
const documentContentPage = new DocumentContentPage(page)
await test.step('2. Add reviewers from team', async () => {
await documentContentPage.addReviewersFromTeam()
await documentContentPage.sendForReview()
await documentContentPage.completeReview()
})
await test.step('3. Send for Approval', async () => {
await documentContentPage.buttonSendForApproval.click()
await documentContentPage.fillSelectApproversForm([documentDetails.owner])
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
await documentContentPage.checkDocument({
...documentDetails,
status: DocumentStatus.IN_APPROVAL
})
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
})
await test.step('4. Approve document', async () => {
await documentContentPage.confirmApproval()
})
await test.step('5. Check the document and status', async () => {
await documentContentPage.checkDocumentStatus(DocumentStatus.EFFECTIVE)
await documentContentPage.checkDocument({
...documentDetails,
status: DocumentStatus.EFFECTIVE,
version: 'v0.1'
})
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
await documentContentPage.openApprovals()
const documentApprovalsPage = new DocumentApprovalsPage(page)
await documentApprovalsPage.checkSuccessApproval(documentDetails.owner)
await attachScreenshot('TESTS-277_approve_document.png', page)
})
await test.step('6. Show Full screen preview', async () => {
await documentContentPage.clickDocumentThreeDots()
const pdfPages = new PdfPages(page)
await pdfPages.printToPdfClick()
await waitForNetworIdle(page)
await page.waitForTimeout(2000)
await pdfPages.showFullScreenPdfClick()
await page.waitForTimeout(2000)
})
await test.step('7. Check PDF Preview', async () => {
const visualCheck = new VisualCheck(page)
await visualCheck.comparePDFPreview('TESTS-277_pdf_preview')
})
await attachScreenshot('TESTS-277_pdf_preview.png', page)
})
})
@@ -0,0 +1,29 @@
import { test } from '@playwright/test'
import { HomepageURI, PlatformSettingThird, PlatformURI } from '../utils'
import { allure } from 'allure-playwright'
import { DocumentContentPage } from '../model/documents/document-content-page'
test.use({
storageState: PlatformSettingThird
})
test.describe('ISO 13485, 4.2.4 Control of documents ensure that documents of external origin are identified and their distribution controlled', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished()
})
test('TESTS-389. As a non workspace owner, I cannot remove a user from workspace', async ({ page }) => {
await allure.description(
'Requirement\nUser is not a part of space members and cannot see or edit any document from that space'
)
await allure.tms('TESTS-389', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-389')
await test.step('2. check if non member can kick user from space', async () => {
const documentContentPage = new DocumentContentPage(page)
await documentContentPage.clickContacts()
await documentContentPage.clickEmployee()
await documentContentPage.selectEmployee('AQ Admin Qara')
await documentContentPage.clickEmployeeDropdown()
await documentContentPage.checkIfUserCanKick()
})
})
})
@@ -0,0 +1,35 @@
import { test } from '@playwright/test'
import { PlatformUser } from '../utils'
import { LoginPage } from '../model/login-page'
test.describe('login test', () => {
let loginPage: LoginPage
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page)
await loginPage.goto()
})
test('check login', async () => {
await loginPage.clickOnLoginWithPassword()
await loginPage.login(PlatformUser, '1234')
})
test('TESTS-392 - As a non workspace user account I cannot log into TraceX', async () => {
await loginPage.clickOnLoginWithPassword()
await loginPage.login('Wrong User', 'Wrong password')
await loginPage.checkIfErrorMessageIsShown('Account not found')
})
test('TESTS-396 - Correct email and wrong password: I cannot log in', async () => {
await loginPage.clickOnLoginWithPassword()
await loginPage.login(PlatformUser, 'Wrong password')
await loginPage.checkIfErrorMessageIsShown('Invalid password')
})
test('TESTS-397 - Wrong email and correct password: I cannot log in', async () => {
await loginPage.clickOnLoginWithPassword()
await loginPage.login('Wrong User', '1234')
await loginPage.checkIfErrorMessageIsShown('Account not found')
})
})
@@ -0,0 +1,54 @@
import { test } from '@playwright/test'
import { attachScreenshot, HomepageURI, PlatformSettingSecond, PlatformURI, waitForNetworIdle } from '../utils'
import { allure } from 'allure-playwright'
import { DocumentContentPage } from '../model/documents/document-content-page'
import { faker } from '@faker-js/faker'
test.use({
storageState: PlatformSettingSecond
})
test.describe('ISO 13485, 4.2.4 Control of documents ensure that documents of external origin are identified and their distribution controlled', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished()
})
test('TESTS-391. As a non space member, I cannot see nor edit any doc from that space', async ({ page }) => {
await allure.description(
'Requirement\nUser is not a part of space members and cannot see or edit any document from that space'
)
await allure.tms('TESTS-391', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-391')
await test.step('2. check if non member can see space', async () => {
const folderName = faker.word.words(1)
const documentContentPage = new DocumentContentPage(page)
await documentContentPage.clickAddFolderButton()
await documentContentPage.fillDocumentSpaceFormManager(folderName)
await documentContentPage.changeDocumentSpaceMembers(folderName)
await documentContentPage.checkIfTheSpaceIsVisible(folderName, false)
await documentContentPage.clickDocumentsSpace()
await waitForNetworIdle(page)
await documentContentPage.clickOnTeamspaceOrArrow()
await documentContentPage.fillTeamspaceFormManager(folderName)
await documentContentPage.changeTeamspaceMembers(folderName)
await documentContentPage.checkIfTheSpaceIsVisible(folderName, false)
await attachScreenshot('TESTS-391_space_not_existing.png', page)
})
})
test('TESTS-388. As a workspace owner, I can remove a user from workspace', async ({ page }) => {
await allure.description(
'Requirement\nUser is not a part of space members and cannot see or edit any document from that space'
)
await allure.tms('TESTS-388', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-388')
await test.step('2. check if owner can kick user from workspace', async () => {
const documentContentPage = new DocumentContentPage(page)
await documentContentPage.clickContacts()
await documentContentPage.clickEmployee()
await documentContentPage.selectEmployee('KA Karmin Armin')
await documentContentPage.clickEmployeeDropdown()
await documentContentPage.clickKickEmployee()
await documentContentPage.clickConfirmKickEmployee()
await documentContentPage.checkIfEmployeeIsKicked('Karmin Armin')
})
})
})
@@ -0,0 +1,289 @@
import { devices, test } from '@playwright/test'
import {
attachScreenshot,
DocumentURI,
generateId,
getSecondPage,
HomepageURI,
PlatformSetting,
PlatformURI
} from '../utils'
import { allure } from 'allure-playwright'
import { DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../model/types'
import { DocumentContentPage } from '../model/documents/document-content-page'
import { prepareDocumentStep } from './common-documents-steps'
import { DocumentApprovalsPage } from '../model/documents/document-approvals-page'
import { PdfPages } from '../model/documents/pdf-pages'
import { VisualCheck } from '../model/visual-check'
import { DocumentsPage } from '../model/documents/documents-page'
test.use({
storageState: PlatformSetting,
...devices['Desktop Edge'],
channel: 'msedge'
})
test.describe('QMS. PDF Download and Preview', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished()
})
test.afterEach(async ({ browser }) => {
const contexts = browser.contexts()
for (const context of contexts) {
await context.close()
}
})
test('TESTS-386 - @PDF Author can Generate PDF from a doc with all Reviewers and Approvers signature info displayed', async ({
page,
browser
}) => {
await allure.description('Requirement\nUsers need to review the document and review status is displayed in PDF')
await allure.tms('TESTS-386', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-386')
const userSecondPage = await getSecondPage(browser)
const approveDocument: NewDocument = {
template: 'HR (HR)',
title: 'This is a test document QMS TESTS-386',
description: 'This is a test document description'
}
const documentDetails: DocumentDetails = {
type: 'HR',
category: 'Human Resources',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
}
await prepareDocumentStep(page, approveDocument)
const documentContentPage = new DocumentContentPage(page)
await test.step('2. Add reviewers from team', async () => {
await documentContentPage.addReviewersFromTeam(true)
await documentContentPage.sendForReview()
await documentContentPage.completeReview()
})
await test.step('3. Send for Approval', async () => {
await (await userSecondPage.goto(`${PlatformURI}/${DocumentURI}`))?.finished()
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
const documentsPageSecond = new DocumentsPage(userSecondPage)
await documentsPageSecond.openDocument(approveDocument.title)
await documentContentPageSecond.completeReview()
await documentsPageSecond.openDocument(approveDocument.title)
await documentContentPage.buttonSendForApproval.click()
await documentContentPage.fillSelectApproversForm([documentDetails.owner])
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
await documentContentPage.checkDocument({
...documentDetails,
status: DocumentStatus.IN_APPROVAL
})
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
})
await test.step('4. Approve document', async () => {
await documentContentPage.confirmApproval()
})
await test.step('5. Check the document and status', async () => {
await documentContentPage.checkDocumentStatus(DocumentStatus.EFFECTIVE)
await documentContentPage.checkDocument({
...documentDetails,
status: DocumentStatus.EFFECTIVE,
version: 'v0.1'
})
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
await documentContentPage.openApprovals()
const documentApprovalsPage = new DocumentApprovalsPage(page)
await documentApprovalsPage.checkSuccessApproval(documentDetails.owner)
await attachScreenshot('TESTS-386_approve_document.png', page)
})
const documentsPageSecond = new DocumentsPage(userSecondPage)
await documentsPageSecond.openDocument(approveDocument.title)
await test.step('6. Show Full screen preview', async () => {
const pdfPagesSecondPage = new PdfPages(userSecondPage)
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
await documentContentPageSecond.clickDocumentThreeDots()
await pdfPagesSecondPage.printToPdfClick()
await userSecondPage.waitForTimeout(2000)
await pdfPagesSecondPage.showFullScreenPdfClick()
await userSecondPage.waitForTimeout(2000)
})
await test.step('7. Check PDF Preview', async () => {
const visualCheckSecondPage = new VisualCheck(userSecondPage)
await visualCheckSecondPage.comparePDFPreview('TESTS-386_pdf_preview')
})
await attachScreenshot('TESTS-386.png', page)
})
test('TESTS-387 - @PDF Author can Generate PDF from a doc with all Reviewers and Approvers signature info displayed', async ({
page,
browser
}) => {
await allure.description('Requirement\nUsers need to review the document and review status is displayed in PDF')
await allure.tms('TESTS-387', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-387')
const userSecondPage = await getSecondPage(browser)
const approveDocument: NewDocument = {
template: 'HR (HR)',
title: 'This is a test for a QMS TESTS-387',
description: 'This is a test document description'
}
const documentDetails: DocumentDetails = {
type: 'HR',
category: 'Human Resources',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
}
await prepareDocumentStep(page, approveDocument)
const documentContentPage = new DocumentContentPage(page)
await test.step('2. Add reviewers from team', async () => {
await documentContentPage.addReviewersFromTeam(false)
await documentContentPage.addApproversFromTeam()
await documentContentPage.buttonSendForApproval.click()
await documentContentPage.fillSelectApproversForm([documentDetails.owner])
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
await documentContentPage.checkDocument({
...documentDetails,
status: DocumentStatus.IN_APPROVAL
})
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
await documentContentPage.confirmApproval()
})
await test.step('4. Send for Approval', async () => {
await (await userSecondPage.goto(`${PlatformURI}/${DocumentURI}`))?.finished()
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
const documentsPageSecond = new DocumentsPage(userSecondPage)
await documentsPageSecond.openDocument(approveDocument.title)
await documentContentPageSecond.clickApproveButtonAndFillPassword()
// await documentContentPageSecond.completeReview()
await documentsPageSecond.openDocument(approveDocument.title)
})
await test.step('7. Show Full screen preview', async () => {
const documentsPageSecond = new DocumentsPage(userSecondPage)
const pdfPagesSecondPage = new PdfPages(userSecondPage)
await documentsPageSecond.openDocument(approveDocument.title)
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
await documentContentPageSecond.clickDocumentThreeDots()
await pdfPagesSecondPage.printToPdfClick()
await userSecondPage.waitForTimeout(2000)
await pdfPagesSecondPage.showFullScreenPdfClick()
await userSecondPage.waitForTimeout(2000)
})
await test.step('8. Check PDF Preview', async () => {
const visualCheckSecondPage = new VisualCheck(userSecondPage)
await visualCheckSecondPage.comparePDFPreview('TESTS-387_pdf_preview')
})
await attachScreenshot('TESTS-387.png', page)
})
test('TESTS-393 - Doc Author, Reviewers, Approvers Electronic signature once doc is Effective', async ({
page,
browser
}) => {
await allure.description('Requirement\nUsers need to review the document and review status is displayed in PDF')
await allure.tms('TESTS-393', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-393')
const userSecondPage = await getSecondPage(browser)
const approveDocument: NewDocument = {
template: 'HR (HR)',
title: `Complete document-${generateId()}`,
description: `Complete document description-${generateId()}`
}
const documentDetails: DocumentDetails = {
type: 'HR',
category: 'Human Resources',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
}
await prepareDocumentStep(page, approveDocument)
const documentContentPage = new DocumentContentPage(page)
await test.step('2. Add reviewers from team', async () => {
await documentContentPage.addReviewersFromTeam(false)
await documentContentPage.addApproversFromTeam()
await documentContentPage.buttonSendForApproval.click()
await documentContentPage.fillSelectApproversForm([documentDetails.owner])
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
await documentContentPage.checkIfLeftModalIsOpen()
await documentContentPage.checkDocument({
...documentDetails,
status: DocumentStatus.IN_APPROVAL
})
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
await documentContentPage.confirmApproval()
})
await test.step('4. Send for Approval', async () => {
await (await userSecondPage.goto(`${PlatformURI}/${DocumentURI}`))?.finished()
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
const documentsPageSecond = new DocumentsPage(userSecondPage)
await documentsPageSecond.openDocument(approveDocument.title)
await documentContentPageSecond.clickApproveButton()
await documentsPageSecond.openDocument(approveDocument.title)
})
await test.step('5. check if reviewers and approvers are visible', async () => {
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
await documentContentPageSecond.clickOpenTeam()
await documentContentPageSecond.checkIfReviewersAndApproversAreVisible()
})
await attachScreenshot('TESTS-393.png', page)
})
test('TESTS-394 - Doc Author, Reviewers, Approvers Electronic signature once doc is Effective', async ({
page,
browser
}) => {
await allure.description('Requirement\nUsers need to review the document and review status is displayed in PDF')
await allure.tms('TESTS-394', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-394')
const userSecondPage = await getSecondPage(browser)
const approveDocument: NewDocument = {
template: 'HR (HR)',
title: `Complete document-${generateId()}`,
description: `Complete document description-${generateId()}`
}
const documentDetails: DocumentDetails = {
type: 'HR',
category: 'Human Resources',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
}
await prepareDocumentStep(page, approveDocument)
const documentContentPage = new DocumentContentPage(page)
await test.step('2. Add reviewers from team', async () => {
await documentContentPage.addReviewersFromTeam(false)
await documentContentPage.addApproversFromTeam()
await documentContentPage.buttonSendForApproval.click()
await documentContentPage.fillSelectApproversForm([documentDetails.owner])
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
await documentContentPage.confirmApproval()
})
await test.step('4. Send for Approval', async () => {
await (await userSecondPage.goto(`${PlatformURI}/${DocumentURI}`))?.finished()
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
const documentsPageSecond = new DocumentsPage(userSecondPage)
await documentsPageSecond.openDocument(approveDocument.title)
await documentContentPageSecond.clickApproveButton()
await documentsPageSecond.openDocument(approveDocument.title)
})
await test.step('5. check if reviewers and approvers are visible', async () => {
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
await documentContentPageSecond.clickOpenTeam()
await documentContentPageSecond.checkTheUserCantChangeReviewersAndApprovers()
})
await attachScreenshot('TESTS-394.png', page)
})
})
@@ -1,17 +1,20 @@
import { test } from '@playwright/test'
import { attachScreenshot, generateId, HomepageURI, PlatformSetting, PlatformURI } from '../utils'
import { devices, test } from '@playwright/test'
import { attachScreenshot, HomepageURI, PlatformSetting, PlatformURI } from '../utils'
import { allure } from 'allure-playwright'
import { DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../model/types'
import { DocumentContentPage } from '../model/documents/document-content-page'
import { prepareDocumentStep } from './common-documents-steps'
import { DocumentApprovalsPage } from '../model/documents/document-approvals-page'
import { PdfPages } from '../model/documents/pdf-pages'
import { VisualCheck } from '../model/visual-check'
test.use({
storageState: PlatformSetting
storageState: PlatformSetting,
...devices['Desktop Edge'],
channel: 'msedge'
})
test.describe('QMS. PDF Download', () => {
test.describe('@PDF. QMS. PDF Download and Preview', () => {
test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished()
})
@@ -28,8 +31,8 @@ test.describe('QMS. PDF Download', () => {
await allure.tms('TESTS-271', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-271')
const approveDocument: NewDocument = {
template: 'HR (HR)',
title: `Approve document-${generateId()}`,
description: `Approve document description-${generateId()}`
title: 'Approve document-Test',
description: 'Approve document description-Test}'
}
const documentDetails: DocumentDetails = {
type: 'HR',
@@ -79,4 +82,157 @@ test.describe('QMS. PDF Download', () => {
})
await attachScreenshot('TESTS-271_ddownloaded_document.png', page)
})
test('TESTS-272. Check PDF Preview', async ({ page }) => {
await allure.description('Requirement\nUsers need to approve the document and check PDF preview')
await allure.tms('TESTS-271', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-272')
const approveDocument: NewDocument = {
template: 'HR (HR)',
title: 'Approve document-Test 2}',
description: 'Approve document description-Test 2}'
}
const documentDetails: DocumentDetails = {
type: 'HR',
category: 'Human Resources',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
}
await prepareDocumentStep(page, approveDocument)
const documentContentPage = new DocumentContentPage(page)
await test.step('2. Send for Approval', async () => {
await documentContentPage.buttonSendForApproval.click()
await documentContentPage.fillSelectApproversForm([documentDetails.owner])
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
await documentContentPage.checkDocument({
...documentDetails,
status: DocumentStatus.IN_APPROVAL
})
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
})
await test.step('3. Approve document', async () => {
await documentContentPage.confirmApproval()
})
await test.step('4. Check the document and status', async () => {
await documentContentPage.checkDocumentStatus(DocumentStatus.EFFECTIVE)
await documentContentPage.checkDocument({
...documentDetails,
status: DocumentStatus.EFFECTIVE,
version: 'v0.1'
})
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
await documentContentPage.openApprovals()
const documentApprovalsPage = new DocumentApprovalsPage(page)
await documentApprovalsPage.checkSuccessApproval(documentDetails.owner)
await attachScreenshot('TESTS-272_approve_document.png', page)
})
await test.step('5. Show Full screen preview', async () => {
await documentContentPage.clickDocumentThreeDots()
const pdfPages = new PdfPages(page)
await pdfPages.printToPdfClick()
await page.waitForTimeout(2000)
await pdfPages.showFullScreenPdfClick()
await page.waitForTimeout(2000)
})
await test.step('6. Check PDF Preview', async () => {
const visualCheck = new VisualCheck(page)
await visualCheck.comparePDFPreview('TESTS-272_pdf_preview')
})
await attachScreenshot('TESTS-272_pdf_preview.png', page)
})
// This works fine but have no test case for it
test.skip('TESTS-273. Check PDF Preview', async ({ page, context }) => {
await allure.description('Requirement\nUsers need to approve the document and check PDF preview')
await allure.tms('TESTS-271', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-273')
const approveDocument: NewDocument = {
template: 'HR (HR)',
title: 'Approve document-Test 3}',
description: 'Approve document description-Test 3}'
}
const documentDetails: DocumentDetails = {
type: 'HR',
category: 'Human Resources',
version: 'v0.1',
status: DocumentStatus.DRAFT,
owner: 'Appleseed John',
author: 'Appleseed John'
}
await prepareDocumentStep(page, approveDocument)
const documentContentPage = new DocumentContentPage(page)
await test.step('2. Send for Approval', async () => {
await documentContentPage.buttonSendForApproval.click()
await documentContentPage.fillSelectApproversForm([documentDetails.owner])
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL)
await documentContentPage.checkDocument({ ...documentDetails, status: DocumentStatus.IN_APPROVAL })
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
})
await test.step('3. Approve document', async () => {
await documentContentPage.confirmApproval()
})
await test.step('4. Check the document and status', async () => {
await documentContentPage.checkDocumentStatus(DocumentStatus.EFFECTIVE)
await documentContentPage.checkDocument({ ...documentDetails, status: DocumentStatus.EFFECTIVE, version: 'v0.1' })
await documentContentPage.checkCurrentRights(DocumentRights.VIEWING)
await documentContentPage.openApprovals()
const documentApprovalsPage = new DocumentApprovalsPage(page)
await documentApprovalsPage.checkSuccessApproval(documentDetails.owner)
await attachScreenshot('TESTS-271_approve_document.png', page)
})
await test.step('5. Download PDF', async () => {
await documentContentPage.clickDocumentThreeDots()
const pdfPages = new PdfPages(page)
await pdfPages.printToPdfClick()
// Wait for the iframe and log its presence
const iframe = await page
.waitForSelector('iframe[src*="PDF%20print%20preview"]', { timeout: 30000 })
.catch(() => null)
if (iframe === null || iframe === undefined) {
await attachScreenshot('TESTS-273_iframe_not_found.png', page)
throw new Error('iframe not found')
}
// Extract the URL from the iframe
const iframeURL = await iframe.getAttribute('src')
if (iframeURL !== null && iframeURL !== undefined) {
const newTab = await context.newPage()
try {
await newTab.goto(iframeURL, { timeout: 60000, waitUntil: 'domcontentloaded' })
await newTab.waitForTimeout(1000)
const visualCheck = new VisualCheck(newTab)
await visualCheck.compareScreenshot(null, 'TESTS-273_pdf_preview')
await newTab.close()
} catch (error) {
await attachScreenshot('TESTS-273_navigation_error.png', newTab)
await newTab.close()
throw error
}
} else {
throw new Error('URL not found in iframe')
}
})
await attachScreenshot('TESTS-273_final_state.png', page)
})
})
@@ -97,11 +97,11 @@ test.describe('ISO 13485, 4.2.4 Control of documents', () => {
})
})
test.skip('TESTS-271. Generate a PDF from an Effective doc', async ({ page, browser }) => {
test('TESTS-272. @PDF Generate a PDF from an Effective doc', async ({ page, browser }) => {
await allure.description(
'Requirement\nUsers need to make a resolve all comments and done documents for the Effective status'
)
await allure.tms('TESTS-271', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-271')
await allure.tms('TESTS-272', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-272')
const userSecondPage = await getSecondPage(browser)
const completeDocument: NewDocument = {
@@ -152,11 +152,7 @@ test.describe('ISO 13485, 4.2.4 Control of documents', () => {
})
await documentContentPageSecond.checkCurrentRights(DocumentRights.VIEWING)
await attachScreenshot('TESTS-271_approve_document.png', page)
})
await test.step('4. Download PDF', async () => {
await attachScreenshot('TESTS-271-check_content.png', page)
await attachScreenshot('TESTS-272_approve_document.png', page)
})
})
})
@@ -5,6 +5,7 @@ import { DocumentContentPage } from '../model/documents/document-content-page'
import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { faker } from '@faker-js/faker'
import { SettingsPage } from '../model/setting-page'
test.use({
storageState: PlatformSetting
@@ -60,4 +61,48 @@ test.describe('ISO 13485, 4.2.4 Control of documents', () => {
await attachScreenshot('TESTS-381_document_space_created.png', page)
})
test('TESTS-406. As a space member only, I cannot delete any doc from that space', async ({ page }) => {
await allure.description('Requirement\nUsers need to create a new space')
await allure.tms('TESTS-406', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-406')
const leftSideMenuPage = new LeftSideMenuPage(page)
const folderName = generateId(5)
await leftSideMenuPage.clickButtonOnTheLeft('Documents')
await test.step('2. Create a new document space and check if user can create document', async () => {
const documentContentPage = new DocumentContentPage(page)
await documentContentPage.clickAddFolderButton()
await documentContentPage.fillDocumentAndSetMember(folderName)
await documentContentPage.checkIfUserCanCreateDocument(folderName)
})
await attachScreenshot('TESTS-406_user_cant_see_workspace.png', page)
})
test('TESTS-342. As a workspace owner, I can create roles and setup permissions', async ({ page }) => {
await allure.description(
'Requirement\nUser is the owner of the workspace and can create roles and setup permissions'
)
await allure.tms('TESTS-342', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-342')
await test.step('2. Check user role and if the update permission is disabled', async () => {
const settingsPage = new SettingsPage(page)
await settingsPage.openProfileMenu()
await settingsPage.clickSettings()
await settingsPage.clickDefaultDocuments()
await settingsPage.chooseRole('Manager')
await settingsPage.checkIfPermissionsExist()
await settingsPage.checkIfAddUpdateDocumentOwnerPermissionIsDisabled()
await attachScreenshot('TESTS-342_Manager_roles.png', page)
await settingsPage.clickDefaultDocuments()
await settingsPage.chooseRole('QARA')
await settingsPage.checkIfPermissionsExist()
await settingsPage.checkIfAddUpdateDocumentOwnerPermissionIsDisabled()
await attachScreenshot('TESTS-342_QARA_roles.png', page)
await settingsPage.clickDefaultDocuments()
await settingsPage.chooseRole('Qualified User')
await settingsPage.checkPermissionsExistQualifyUser()
await settingsPage.checkIfAddUpdateDocumentOwnerPermissionIsDisabled()
await attachScreenshot('TESTS-342_User_roles.png', page)
})
})
})
@@ -1,5 +1,13 @@
import { test } from '@playwright/test'
import { attachScreenshot, generateId, HomepageURI, PlatformSettingSecond, PlatformURI } from '../utils'
import {
attachScreenshot,
DocumentURI,
generateId,
getSecondPage,
HomepageURI,
PlatformSettingSecond,
PlatformURI
} from '../utils'
import { allure } from 'allure-playwright'
import { SettingsPage } from './../model/setting-page'
@@ -152,4 +160,29 @@ test.describe('ISO 13485, 4.2.4 Control of documents ensure that documents of ex
})
await attachScreenshot('TESTS-405_status_is_deleted.png', page)
})
test('TESTS-390. As a workspace admin, I can assign a user to any private space (e.g. Task, Controlled doc, Product, Training)', async ({
page,
browser
}) => {
await allure.description(
'Requirement\nUser is not a part of space members and cannot see or edit any document from that space'
)
await allure.tms('TESTS-390', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-390')
const folderName = faker.word.words(1)
const userSecondPage = await getSecondPage(browser)
const documentContentPage = new DocumentContentPage(page)
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
await (await userSecondPage.goto(`${PlatformURI}/${DocumentURI}`))?.finished()
await test.step('2. create a new space', async () => {
await documentContentPage.clickDocumentsSpace()
await documentContentPage.clickOnTeamspaceOrArrow()
await documentContentPage.fillDocumentAndSetMemberPrivate(folderName)
await test.step('2. check if user can see space', async () => {
await documentContentPageSecond.clickDocumentsSpace()
await documentContentPageSecond.checkIfTheSpaceIsVisible(folderName, true)
await attachScreenshot('TESTS-391_space_not_existing.png', userSecondPage)
})
})
})
})
@@ -84,6 +84,21 @@ export class DocumentContentPage extends DocumentCommonPage {
readonly createNewTemplateFromSpace: Locator
readonly okButton: Locator
readonly documentThreeDots: Locator
readonly buttonSubmitReview: Locator
readonly completeReviewButton: Locator
readonly approveButton: Locator
readonly documentsSpace: Locator
readonly contacts: Locator
readonly createTeamspace: Locator
readonly employee: Locator
readonly employeeDropdown: Locator
readonly kickEmployee: Locator
readonly confirmKickEmployee: Locator
readonly openTeam: Locator
readonly privateToggle: Locator
readonly inputTeamspaceName: Locator
readonly teamspaceArrow: Locator
readonly infoModal: Locator
constructor (page: Page) {
super(page)
@@ -141,7 +156,7 @@ export class DocumentContentPage extends DocumentCommonPage {
this.roleSelector = page.getByRole('button', { name: 'Members' })
this.selectRoleMemberAJ = page.getByRole('button', { name: 'AJ Appleseed John' })
this.selectRoleMemberDK = page.getByRole('button', { name: 'DK Dirak Kainin' })
this.createButton = page.getByRole('button', { name: 'Create' })
this.createButton = page.getByRole('button', { name: 'Create', exact: true })
this.createNewDocument = page.getByRole('button', { name: 'Create new document' })
this.selectCustom = page.getByText('Custom')
this.customSpecificReason = page.getByPlaceholder('Specify the reason...')
@@ -178,12 +193,85 @@ export class DocumentContentPage extends DocumentCommonPage {
this.createNewTemplateFromSpace = page.getByRole('button', { name: 'Create new template' })
this.okButton = page.getByRole('button', { name: 'Ok', exact: true })
this.documentThreeDots = page.locator("div[class='no-print ml-1'] button[type='button']")
this.buttonSubmitReview = page.getByRole('button', { name: 'Submit' })
this.completeReviewButton = page.getByRole('button', { name: 'Complete review' })
this.approveButton = page.getByRole('button', { name: 'Approve' })
this.documentsSpace = page.locator('[id="app-document\\:string\\:DocumentApplication"]')
this.contacts = page.locator('[id="app-contact\\:string\\:Contacts"]')
this.createTeamspace = page.getByRole('button', { name: 'Create teamspace' })
this.employee = page.getByRole('button', { name: 'Employee' })
this.employeeDropdown = page.locator('div:nth-child(4) > button').first()
this.kickEmployee = page.getByRole('button', { name: 'Kick employee' })
this.confirmKickEmployee = page.getByRole('button', { name: 'Ok' })
this.openTeam = page.getByText('Team')
this.privateToggle = page.locator('#teamspace-private span')
this.inputTeamspaceName = page.getByPlaceholder('New teamspace')
this.teamspaceArrow = page.locator('.w-full > button:nth-child(2)')
this.infoModal = page.locator('#btnGID-info')
}
async checkDocumentTitle (title: string): Promise<void> {
await expect(this.buttonDocumentTitle).toContainText(title)
}
async clickDocumentsSpace (): Promise<void> {
await this.documentsSpace.click()
}
async clickCreateTeamspace (): Promise<void> {
await this.createTeamspace.click()
}
async clickContacts (): Promise<void> {
await this.contacts.click()
}
async clickEmployee (): Promise<void> {
await this.employee.click()
}
async selectEmployee (employee: string): Promise<void> {
await this.page.getByRole('link', { name: employee }).click()
}
async clickEmployeeDropdown (): Promise<void> {
await this.employeeDropdown.click()
}
async clickKickEmployee (): Promise<void> {
await this.kickEmployee.click()
}
async clickConfirmKickEmployee (): Promise<void> {
await this.confirmKickEmployee.click()
}
async checkIfEmployeeIsKicked (employee: string): Promise<void> {
await this.page.getByRole('link', { name: 'Employee' }).getByRole('button').first().click()
await expect(this.page.getByText(employee + ' Inactive')).toBeVisible()
}
async checkIfUserCanKick (): Promise<void> {
await expect(this.kickEmployee).not.toBeVisible()
}
async clickOpenTeam (): Promise<void> {
await this.openTeam.click()
}
async checkIfReviewersAndApproversAreVisible (): Promise<void> {
await expect(this.page.getByText('Appleseed John').first()).toBeVisible()
await expect(this.page.getByText('Dirak Kainin')).toBeVisible()
await expect(this.page.getByText('Appleseed John').nth(1)).toBeVisible()
}
async checkTheUserCantChangeReviewersAndApprovers (): Promise<void> {
await this.page.getByText('Appleseed John').first().click()
await expect(this.page.getByText('Dirak Kainin').nth(1)).not.toBeVisible()
await this.page.getByText('Dirak Kainin').click()
await expect(this.page.getByText('Dirak Kainin').nth(1)).not.toBeVisible()
}
async clickDocumentHeader (name: string): Promise<void> {
await this.page.getByRole('button', { name }).click()
}
@@ -257,6 +345,16 @@ export class DocumentContentPage extends DocumentCommonPage {
await this.createButton.click()
}
async clickApproveButtonAndFillPassword (): Promise<void> {
await this.approveButton.click()
await this.inputPassword.fill(PlatformPassword)
await this.buttonSubmit.click()
}
async clickApproveButton (): Promise<void> {
await this.approveButton.click()
}
async expectCategoryCreated (categoryTitle: string, categoryCode: string): Promise<void> {
await expect(this.page.getByText(categoryTitle)).toBeVisible()
await expect(this.page.getByRole('link', { name: categoryCode })).toBeVisible()
@@ -287,6 +385,29 @@ export class DocumentContentPage extends DocumentCommonPage {
}
}
async addReviewersFromTeam (anotherReviewer: boolean = false): Promise<void> {
await this.page.waitForTimeout(500)
await this.page.getByText('Team').click()
await this.page.getByText('Add member').nth(1).click()
if (anotherReviewer) {
await this.page.getByRole('button', { name: 'DK Dirak Kainin' }).click()
}
await this.selectRoleMemberAJ.click()
await this.page.keyboard.press('Escape')
}
async addApproversFromTeam (): Promise<void> {
await this.page.waitForTimeout(500)
await this.page.getByText('Add member').nth(2).click()
await this.page.getByRole('button', { name: 'DK Dirak Kainin' }).click()
await this.page.keyboard.press('Escape')
}
async sendForReview (): Promise<void> {
await this.buttonSendForReview.click()
await this.buttonSubmitReview.click()
}
async addContent (content: string, append: boolean = false, newParagraph: boolean = false): Promise<void> {
if (newParagraph) {
await this.contentLocator.press('Enter')
@@ -357,6 +478,44 @@ export class DocumentContentPage extends DocumentCommonPage {
await this.createButton.click()
}
async fillDocumentAndSetMember (spaceName: string): Promise<void> {
await this.inputSpaceName.fill(spaceName)
await this.page.getByRole('button', { name: 'AJ Appleseed John' }).first().click()
await this.page.getByRole('button', { name: 'AQ Admin Qara' }).click()
await this.page.getByRole('button', { name: 'AJ Appleseed John' }).nth(1).click()
await this.page.keyboard.press('Escape')
await this.page.getByRole('button', { name: 'Create' }).click()
}
async checkIfUserCanCreateDocument (spaceName: string): Promise<void> {
await this.page.getByRole('button', { name: 'New document', exact: true }).click()
await this.page.locator('[id="space\\.selector"]').click()
await expect(this.page.locator('.selectPopup').getByRole('button', { name: spaceName })).not.toBeVisible()
}
async fillDocumentAndSetMemberPrivate (spaceName: string): Promise<void> {
await this.inputTeamspaceName.fill(spaceName)
await this.privateToggle.click()
await this.page.getByRole('button', { name: 'DK Dirak Kainin' }).nth(1).click()
await this.page.getByRole('button', { name: 'AJ Appleseed John' }).click()
await this.page.keyboard.press('Escape')
await this.page.getByRole('button', { name: 'Create', exact: true }).click()
}
async clickTeamspaceArrow (): Promise<void> {
await this.teamspaceArrow.click()
}
async clickOnTeamspaceOrArrow (): Promise<void> {
const teamspaceOrArrow = await this.page.isVisible('.w-full > button:nth-child(2)')
if (teamspaceOrArrow) {
await this.clickTeamspaceArrow()
await this.createTeamspace.click()
} else {
await this.createTeamspace.click()
}
}
async fillQuaraManager (spaceName: string): Promise<void> {
await this.inputSpaceName.fill(spaceName)
await this.roleSelector.nth(2).click()
@@ -375,6 +534,31 @@ export class DocumentContentPage extends DocumentCommonPage {
await this.createButton.click()
}
async fillTeamspaceFormManager (spaceName: string): Promise<void> {
await this.page.getByPlaceholder('New teamspace').fill(spaceName)
// await this.page.getByRole('button', { name: 'DK Dirak Kainin' }).first().click()
// await this.page.getByRole('button', { name: 'DK Dirak Kainin' }).nth(2).click()
// await this.page.keyboard.press('Escape')
await this.page.waitForTimeout(1000)
await this.createButton.click()
}
async changeTeamspaceMembers (spaceName: string): Promise<void> {
await this.page.getByRole('button', { name: spaceName }).hover()
await this.page.getByRole('button', { name: spaceName }).getByRole('button').nth(1).click()
await this.page.getByRole('button', { name: 'Edit teamspace' }).click()
await this.page.getByRole('button', { name: 'DK Dirak Kainin' }).first().click()
await this.page.getByRole('button', { name: 'DK Dirak Kainin' }).nth(2).click()
await this.page.getByRole('button', { name: 'AJ Appleseed John' }).click()
await this.page.keyboard.press('Escape')
await this.page.waitForTimeout(1000)
await this.page.getByRole('button', { name: 'AJ DK 2 members' }).click()
await this.page.getByRole('button', { name: 'DK Dirak Kainin' }).click()
await this.page.keyboard.press('Escape')
await this.page.waitForTimeout(1000)
await this.page.getByRole('button', { name: 'Save' }).click()
}
async changeDocumentSpaceMembers (spaceName: string): Promise<void> {
await this.page.getByRole('button', { name: spaceName }).hover()
await this.page.getByRole('button', { name: spaceName }).getByRole('button').click()
@@ -496,6 +680,13 @@ export class DocumentContentPage extends DocumentCommonPage {
await expect(this.textDocumentStatus).toHaveText(status)
}
async checkIfLeftModalIsOpen (): Promise<void> {
const teamspaceOrArrow = await this.page.isHidden('div.flex:has(div.label:text("Template name")) div.field')
if (teamspaceOrArrow) {
await this.infoModal.click()
}
}
async checkDocument (data: DocumentDetails): Promise<void> {
if (data.type != null && data.type !== 'N/A') {
await expect(this.textType).toHaveText(data.type)
@@ -5,12 +5,14 @@ export class PdfPages {
readonly printToPdf: Locator
readonly printToPdfHeader: Locator
readonly downloadPdf: Locator
readonly showFullScreenPdf: Locator
constructor (page: Page) {
this.page = page
this.printToPdf = page.getByRole('button', { name: 'Print to PDF' })
this.printToPdfHeader = page.getByText('PDF PDF print preview')
this.downloadPdf = page.locator('form').getByRole('link').getByRole('button')
this.showFullScreenPdf = page.locator('form').getByRole('button').nth(2)
}
async printToPdfClick (): Promise<void> {
@@ -18,6 +20,10 @@ export class PdfPages {
await expect(this.printToPdfHeader).toBeVisible()
}
async showFullScreenPdfClick (): Promise<void> {
await this.showFullScreenPdf.click()
}
async downloadAndVerifyPdf (): Promise<void> {
const [download] = await Promise.all([this.page.waitForEvent('download'), this.downloadPdf.click()])
const filePath = await download.path()
+17 -1
View File
@@ -8,7 +8,8 @@ export class LoginPage {
readonly buttonLogin: Locator
readonly buttonSignUp: Locator
readonly loginWithPassword: Locator
readonly invalidPasswordMessage: Locator
readonly accountNotFoundMessage: Locator
constructor (page: Page) {
this.page = page
this.inputEmail = page.locator('input[name=email]')
@@ -16,6 +17,8 @@ export class LoginPage {
this.buttonLogin = page.locator('button', { hasText: 'Log In' })
this.buttonSignUp = page.locator('a.title', { hasText: 'Sign Up' })
this.loginWithPassword = page.locator('a', { hasText: 'Login with password' })
this.invalidPasswordMessage = page.getByText('Invalid password')
this.accountNotFoundMessage = page.getByText('Account not found')
}
async goto (): Promise<void> {
@@ -29,6 +32,19 @@ export class LoginPage {
await this.buttonLogin.click()
}
async checkIfErrorMessageIsShown (message: string): Promise<void> {
if (message === 'Invalid password') {
await expect(this.invalidPasswordMessage).toContainText(message)
}
if (message === 'Account not found') {
await expect(this.accountNotFoundMessage).toContainText(message)
}
}
async clickOnLoginWithPassword (): Promise<void> {
await this.loginWithPassword.click()
}
async checkIfUserIsLoggedIn (credentials: string): Promise<void> {
if (credentials === 'wrong-password') {
await expect(this.buttonLogin).toBeVisible()
@@ -18,6 +18,7 @@ export class SettingsPage {
readonly deleteDocumentCategoryPermission: Locator
readonly updateSpacePermission: Locator
readonly addRoleUpdatePermissionOwner: Locator
readonly addUpdateDocumentOwnerPermission: Locator
constructor (page: Page) {
this.page = page
@@ -37,6 +38,7 @@ export class SettingsPage {
this.deleteDocumentCategoryPermission = page.getByText('Delete document category', { exact: true })
this.updateSpacePermission = page.getByText('Update space', { exact: true })
this.addRoleUpdatePermissionOwner = page.getByRole('button', { name: 'Update document owner' })
this.addUpdateDocumentOwnerPermission = page.locator('.hulyTableAttr-header > .font-medium-14')
}
async openProfileMenu (): Promise<void> {
@@ -69,6 +71,10 @@ export class SettingsPage {
await expect(this.updateSpacePermission).toBeVisible()
}
async checkIfAddUpdateDocumentOwnerPermissionIsDisabled (): Promise<void> {
await expect(this.addUpdateDocumentOwnerPermission).toBeDisabled()
}
async checkPermissionsExistQualifyUser (): Promise<void> {
await expect(this.reviewDocumentPermission).toBeVisible()
await expect(this.approveDocumentPermission).toBeVisible()
@@ -0,0 +1,51 @@
import { expect, Page } from '@playwright/test'
import { join } from 'path'
export class VisualCheck {
readonly page: Page
constructor (page: Page) {
this.page = page
}
async compareScreenshot (selector: string | null, screenshotName: string): Promise<void> {
let screenshot
if (selector !== null && selector !== undefined) {
const element = this.page.locator(selector)
await expect(element).toBeVisible()
screenshot = await element.screenshot()
} else {
screenshot = await this.page.screenshot()
}
// Compare the screenshot with the stored snapshot
expect(screenshot).toMatchSnapshot(join(__dirname, '..', 'screenshots', `${screenshotName}.png`), {
maxDiffPixelRatio: 0.2
})
}
async comparePDFPreview (screenshotName: string): Promise<void> {
// Wait for the iframe to be present in the DOM
const iframe = this.page.locator('iframe.pdfviewer-content')
await iframe.waitFor({ state: 'attached', timeout: 10000 })
// Wait for the iframe to finish loading
await this.page.waitForFunction(
() => {
const iframe = document.querySelector('iframe.pdfviewer-content') as HTMLIFrameElement
return iframe?.contentDocument?.readyState === 'complete'
},
{ timeout: 30000 }
)
const pdfPreviewModal = this.page.locator('.antiDialog > .content')
await expect(pdfPreviewModal).toBeVisible()
const screenshot = await this.page.screenshot()
expect(screenshot).toMatchSnapshot(join(__dirname, '..', 'screenshots', `${screenshotName}.png`), {
maxDiffPixelRatio: 0.2
})
}
}
+4
View File
@@ -74,3 +74,7 @@ export async function * iterateLocator (locator: Locator): AsyncGenerator<Locato
yield locator.nth(index)
}
}
export async function waitForNetworIdle (page: Page, timeout = 2000): Promise<void> {
await Promise.race([page.waitForLoadState('networkidle'), new Promise((resolve) => setTimeout(resolve, timeout))])
}
+17 -2
View File
@@ -30,7 +30,6 @@ import os from 'os'
*/
export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap, onClose?: () => void): void {
console.log('Starting account service with brandings: ', brandings)
const methods = getMethods()
const ACCOUNT_PORT = parseInt(process.env.ACCOUNT_PORT ?? '3000')
const dbUrl = process.env.DB_URL
if (dbUrl === undefined) {
@@ -66,6 +65,17 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap
const productName = process.env.PRODUCT_NAME
const lang = process.env.LANGUAGE ?? 'en'
const wsLivenessDaysRaw = process.env.WS_LIVENESS_DAYS
let wsLivenessDays: number | undefined
if (wsLivenessDaysRaw !== undefined) {
try {
wsLivenessDays = parseInt(wsLivenessDaysRaw)
} catch (err: any) {
// DO NOTHING
}
}
setMetadata(account.metadata.Transactors, transactorUri)
setMetadata(platform.metadata.locale, lang)
setMetadata(account.metadata.ProductName, productName)
@@ -73,6 +83,7 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap
setMetadata(account.metadata.OtpRetryDelaySec, parseInt(process.env.OTP_RETRY_DELAY ?? '60'))
setMetadata(account.metadata.SES_URL, ses)
setMetadata(account.metadata.FrontURL, frontURL)
setMetadata(account.metadata.WsLivenessDays, wsLivenessDays)
setMetadata(serverToken.metadata.Secret, serverSecret)
@@ -81,6 +92,9 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap
setMetadata(toolPlugin.metadata.InitScriptURL, initScriptUrl)
}
const hasSignUp = process.env.DISABLE_SIGNUP !== 'true'
const methods = getMethods(hasSignUp)
const accountsDb = getAccountDB(dbUrl)
const app = new Koa()
@@ -105,7 +119,8 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap
}),
serverSecret,
frontURL,
brandings
brandings,
!hasSignUp
)
void accountsDb.then((res) => {
+9 -4
View File
@@ -212,7 +212,8 @@ export class WorkspaceMongoDbCollection extends MongoDbCollection<Workspace> imp
region: string,
version: Data<Version>,
operation: WorkspaceOperation,
processingTimeoutMs: number
processingTimeoutMs: number,
wsLivenessMs?: number
): Promise<WorkspaceInfo | undefined> {
const pendingCreationQuery: Filter<Workspace>['$or'] = [{ mode: { $in: ['pending-creation', 'creating'] } }]
@@ -233,9 +234,13 @@ export class WorkspaceMongoDbCollection extends MongoDbCollection<Workspace> imp
$or: [{ mode: 'active' }, { mode: { $exists: false } }]
},
versionQuery,
{
lastVisit: { $gt: Date.now() - 24 * 60 * 60 * 1000 }
}
...(wsLivenessMs !== undefined
? [
{
lastVisit: { $gt: Date.now() - wsLivenessMs }
}
]
: [])
]
},
{
+10 -5
View File
@@ -358,7 +358,8 @@ export class WorkspacePostgresDbCollection extends PostgresDbCollection<Workspac
region: string,
version: Data<Version>,
operation: WorkspaceOperation,
processingTimeoutMs: number
processingTimeoutMs: number,
wsLivenessMs?: number
): Promise<WorkspaceInfo | undefined> {
const sqlChunks: string[] = [`SELECT * FROM ${this.name}`]
const whereChunks: string[] = []
@@ -367,7 +368,7 @@ export class WorkspacePostgresDbCollection extends PostgresDbCollection<Workspac
const pendingCreationSql = "mode IN ('pending-creation', 'creating')"
const versionSql =
'("versionMajor" < $1) OR ("versionMajor" = $1 AND "versionMinor" < $2) OR ("versionMajor" = $1 AND "versionMinor" = $2 AND "versionPatch" < $3)'
const pendingUpgradeSql = `(((disabled = FALSE OR disabled IS NULL) AND (mode = 'active' OR mode IS NULL) AND ${versionSql} AND "lastVisit" > $4) OR ((disabled = FALSE OR disabled IS NULL) AND mode = 'upgrading'))`
const pendingUpgradeSql = `(((disabled = FALSE OR disabled IS NULL) AND (mode = 'active' OR mode IS NULL) AND ${versionSql} ${wsLivenessMs !== undefined ? 'AND "lastVisit" > $4' : ''}) OR ((disabled = FALSE OR disabled IS NULL) AND mode = 'upgrading'))`
const operationSql =
operation === 'create'
? pendingCreationSql
@@ -375,7 +376,11 @@ export class WorkspacePostgresDbCollection extends PostgresDbCollection<Workspac
? pendingUpgradeSql
: `(${pendingCreationSql} OR ${pendingUpgradeSql})`
if (operation === 'upgrade' || operation === 'all') {
values.push(version.major, version.minor, version.patch, Date.now() - 24 * 60 * 60 * 1000)
values.push(version.major, version.minor, version.patch)
if (wsLivenessMs !== undefined) {
values.push(Date.now() - wsLivenessMs)
}
}
whereChunks.push(operationSql)
@@ -383,11 +388,11 @@ export class WorkspacePostgresDbCollection extends PostgresDbCollection<Workspac
// to clear them with the worker.
whereChunks.push('(attempts IS NULL OR attempts <= 3)')
whereChunks.push('("lastProcessingTime" IS NULL OR "lastProcessingTime" < $5)')
whereChunks.push(`("lastProcessingTime" IS NULL OR "lastProcessingTime" < $${values.length + 1})`)
values.push(Date.now() - processingTimeoutMs)
if (region !== '') {
whereChunks.push('region = $6')
whereChunks.push(`region = $${values.length + 1}`)
values.push(region)
} else {
whereChunks.push("(region IS NULL OR region = '')")
+13 -5
View File
@@ -1278,8 +1278,10 @@ export async function getPendingWorkspace (
}
// Move to config?
const processingTimeoutMs = 30 * 1000
const wsLivenessDays = getMetadata(accountPlugin.metadata.WsLivenessDays)
const wsLivenessMs = wsLivenessDays !== undefined ? wsLivenessDays * 24 * 60 * 60 * 1000 : undefined
const result = await db.workspace.getPendingWorkspace(region, version, operation, processingTimeoutMs)
const result = await db.workspace.getPendingWorkspace(region, version, operation, processingTimeoutMs, wsLivenessMs)
if (result != null) {
ctx.info('getPendingWorkspace', {
@@ -2334,8 +2336,9 @@ export async function loginWithProvider (
_email: string,
first: string,
last: string,
extra?: Record<string, string>
): Promise<LoginInfo> {
extra?: Record<string, string>,
signUpDisabled: boolean = false
): Promise<LoginInfo | null> {
try {
const email = cleanEmail(_email)
if (last == null) {
@@ -2357,6 +2360,11 @@ export async function loginWithProvider (
}
return result
}
if (signUpDisabled) {
return null
}
const newAccount = await createAcc(ctx, db, branding, email, null, first, last, true, true, extra)
const result = {
@@ -2463,7 +2471,7 @@ export async function deleteWorkspace (
/**
* @public
*/
export function getMethods (): Record<string, AccountMethod> {
export function getMethods (hasSignUp: boolean = true): Record<string, AccountMethod> {
return {
login: wrap(login),
join: wrap(join),
@@ -2478,7 +2486,7 @@ export function getMethods (): Record<string, AccountMethod> {
getInviteLink: wrap(getInviteLink),
getAccountInfo: wrap(getAccountInfo),
getWorkspaceInfo: wrap(getWorkspaceInfo),
createAccount: wrap(createAccount),
...(hasSignUp ? { createAccount: wrap(createAccount) } : {}),
createWorkspace: wrap(createUserWorkspace),
assignWorkspace: wrap(assignWorkspace),
removeWorkspace: wrap(removeWorkspace),

Some files were not shown because too many files have changed in this diff Show More