Merge remote-tracking branch 'origin/develop' into staging
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 |
@@ -32,6 +32,7 @@
|
||||
"AlignCenter": "Align center",
|
||||
"AlignLeft": "Align left",
|
||||
"AlignRight": "Align right",
|
||||
"Download": "Download",
|
||||
"ViewImage": "View image",
|
||||
"ViewOriginal": "View original",
|
||||
"MoreActions": "More actions",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"AlignCenter": "По центру",
|
||||
"AlignLeft": "По левому краю",
|
||||
"AlignRight": "По правому краю",
|
||||
"Download": "Скачать",
|
||||
"ViewImage": "Открыть изображение",
|
||||
"ViewOriginal": "Открыть оригинал",
|
||||
"MoreActions": "Дополнительные действия",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"AlignCenter": "居中对齐",
|
||||
"AlignLeft": "左对齐",
|
||||
"AlignRight": "右对齐",
|
||||
"Download": "下载",
|
||||
"ViewImage": "查看图片",
|
||||
"ViewOriginal": "查看原图",
|
||||
"MoreActions": "更多操作",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"OpenInNewTab": "Открыть в новой вкладке",
|
||||
"NewVersionAvailable": "Доступна новая версия",
|
||||
"PleaseUpdate": "Пожалуйста, обновите приложение",
|
||||
"ServerUnderMaintenance": "Обслуживание сервера",
|
||||
"ServerUnderMaintenance": "Подготовка рабочего пространства для новой версии...",
|
||||
"MobileNotSupported": "Простите, поддержка мобильных устройств скоро будет доступна. Пока воспользуйтесь компьютером.",
|
||||
"LogInAnyway": "Все равно войти",
|
||||
"WorkspaceCreating": "Пространство создается...",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ?? ''))
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 41 KiB |
@@ -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)
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -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()
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))])
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 = '')")
|
||||
|
||||
@@ -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),
|
||||
|
||||