Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2026-04-13 22:27:35 +07:00
48 changed files with 394 additions and 468 deletions
+6
View File
@@ -40441,6 +40441,9 @@ importers:
'@hcengineering/analytics-service':
specifier: workspace:^0.7.19
version: link:../../../foundations/core/packages/analytics-service
'@hcengineering/api-client':
specifier: workspace:^0.7.25
version: link:../../../foundations/core/packages/api-client
'@hcengineering/core':
specifier: workspace:^0.7.26
version: link:../../../foundations/core/packages/core
@@ -40450,6 +40453,9 @@ importers:
'@hcengineering/server-core':
specifier: workspace:^0.7.19
version: link:../../../foundations/server/packages/core
'@hcengineering/server-guest-resources':
specifier: workspace:^0.7.0
version: link:../../../server-plugins/guest-resources
'@hcengineering/server-storage':
specifier: workspace:^0.7.16
version: link:../../../foundations/server/packages/server-storage
+1
View File
@@ -415,6 +415,7 @@ services:
- STATS_URL=http://huly.local:4900
- ACCOUNTS_URL=http://huly.local:3000
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
- FRONT_URL=http://huly.local:8087
sign:
image: hardcoreeng/sign
extra_hosts:
+1
View File
@@ -153,6 +153,7 @@ services:
- MONGO_URL=mongodb://huly.local:27017?compressors=snappy
- 'MONGO_OPTIONS={"appName":"print","maxPoolSize":1}'
- STORAGE_CONFIG=${STORAGE_CONFIG}
- FRONT_URL=http://huly.local:8087
deploy:
resources:
limits:
+2 -22
View File
@@ -331,28 +331,10 @@ export function devTool (
// })
// })
function parseAccountRole (raw: unknown): AccountRole {
const rawRole = typeof raw === 'string' ? raw.trim() : String(raw ?? 'User').trim()
const normalized = rawRole.trim().toLowerCase()
const match = Object.values(AccountRole).find((v) => v.toLowerCase() === normalized)
if (match !== undefined) return match
switch (normalized) {
case 'readonly':
return AccountRole.ReadOnlyGuest
case 'docguest':
return AccountRole.DocGuest
default:
throw new Error(`Unknown role: ${rawRole}`)
}
}
program
.command('assign-workspace <email> <workspace>')
.description('assign workspace')
.option('--role <role>', 'Workspace role (User, Guest, ReadOnlyGuest, DocGuest, Maintainer, Owner, Admin)', 'User')
.action(async (email: string, workspace: string, cmd: { role: string }) => {
.action(async (email: string, workspace: string, cmd) => {
await withAccountDatabase(async (db) => {
console.log(`assigning user ${email} to ${workspace}...`)
try {
@@ -361,12 +343,10 @@ export function devTool (
throw new Error(`Workspace ${workspace} not found`)
}
const role = cmd.role != null ? parseAccountRole(cmd.role) : AccountRole.User
await assignWorkspace(toolCtx, db, null, getToolToken(), {
email,
workspaceUuid: ws.uuid,
role
role: AccountRole.User
})
} catch (err: any) {
console.error(err)
@@ -0,0 +1,96 @@
//
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type Class,
Client,
type Doc,
type DocumentQuery,
type DomainParams,
type DomainRequestOptions,
type DomainResult,
type FindOptions,
type FindResult,
Hierarchy,
ModelDb,
OperationDomain,
type Ref,
type SearchOptions,
type SearchQuery,
type SearchResult,
type Tx,
type TxResult,
type WithLookup
} from '@hcengineering/core'
import type { RestClient } from './types'
export class RestClientAdapter implements Client {
constructor (
private readonly client: RestClient,
private readonly hierarchy: Hierarchy | undefined,
private readonly model: ModelDb | undefined
) {}
async domainRequest<T>(
domain: OperationDomain,
params: DomainParams,
options?: DomainRequestOptions
): Promise<DomainResult<T>> {
return await this.client.domainRequest(domain, params, options)
}
async findAll<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
return await this.client.findAll(_class, query, options)
}
async tx (tx: Tx): Promise<TxResult> {
return await this.client.tx(tx)
}
async findOne<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<WithLookup<T> | undefined> {
return await this.client.findOne(_class, query, options)
}
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
return await this.client.searchFulltext(query, options)
}
async close (): Promise<void> {
// No ned to close the REST client
}
getHierarchy (): Hierarchy {
if (this.hierarchy === undefined) {
throw new Error('Hierarchy is not defined')
}
return this.hierarchy
}
getModel (): ModelDb {
if (this.model === undefined) {
throw new Error('Model is not defined')
}
return this.model
}
}
@@ -13,6 +13,7 @@
// limitations under the License.
//
export { RestClientAdapter } from './adapter'
export { createRestClient, connectRest } from './rest'
export { createRestTxOperations } from './tx'
export * from './types'
@@ -613,54 +613,6 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
return domain === 'tx' ? 'objectSpace' : domain === 'space' ? '_id' : 'space'
}
private mergeDocIdRestriction<T extends Doc>(query: DocumentQuery<T>, allowed: Ref<T>[]): DocumentQuery<T> {
const allowedIds: DocumentQuery<T>['_id'] = { $in: allowed.length === 0 ? [] : allowed }
const prevId = query._id
if (prevId === undefined) {
return { ...query, _id: allowedIds }
}
type WithAnd = DocumentQuery<T> & { $and?: DocumentQuery<T>[] }
const { _id: _drop, $and, ...rest } = query as WithAnd
const andParts: DocumentQuery<T>[] = [...($and ?? []), { _id: prevId }, { _id: allowedIds }]
const merged: DocumentQuery<T> = { ...rest, $and: andParts }
return merged
}
private async applyGuestCollaboratorReadRestriction<T extends Doc>(
ctx: MeasureContext<SessionData>,
_class: Ref<Class<T>>,
domain: Domain,
query: DocumentQuery<T>
): Promise<DocumentQuery<T>> {
const account = ctx.contextData.account
if (
![AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(account.role) ||
isSystem(account, ctx) ||
domain === DOMAIN_MODEL
) {
return query
}
const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, _class)
if (collabSec?.provideSecurity !== true) {
return query
}
const rootClass = collabSec.attachedTo
const docClasses = [...this.context.hierarchy.getDescendants(rootClass), rootClass]
const collabs = (await this.provideFindAll(
ctx,
core.class.Collaborator,
{
collaborator: account.uuid,
attachedToClass: { $in: docClasses }
},
{ projection: { attachedTo: 1 }, limit: 10_000 }
)) as Collaborator[]
const allowed = collabs.map((c) => c.attachedTo) as Ref<T>[]
return this.mergeDocIdRestriction(query, allowed)
}
override async findAll<T extends Doc>(
ctx: MeasureContext<SessionData>,
_class: Ref<Class<T>>,
@@ -727,10 +679,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
}
}
let queryToUse = !this.skipFindCheck ? newQuery : query
queryToUse = await this.applyGuestCollaboratorReadRestriction(ctx, _class, domain, queryToUse)
let findResult = await this.provideFindAll(ctx, _class, queryToUse, options)
let findResult = await this.provideFindAll(ctx, _class, !this.skipFindCheck ? newQuery : query, options)
if (clientFilterSpaces !== undefined) {
const cfs = clientFilterSpaces
findResult = toFindResult(
@@ -1,172 +0,0 @@
//
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import core, {
AccountRole,
MeasureMetricsContext,
generateId,
toFindResult,
type Account,
type AccountUuid,
type Class,
type ClassCollaborators,
type Collaborator,
type Doc,
type Domain,
type MeasureContext,
type PersonId,
type Ref,
type SessionData
} from '@hcengineering/core'
import type { Middleware, PipelineContext } from '@hcengineering/server-core'
import { SpaceSecurityMiddleware } from '../spaceSecurity'
const DOC_CLASS = 'test:class:Doc' as Ref<Class<Doc>>
const DOC_ID = 'test:doc:1' as Ref<Doc>
const TEST_DOMAIN = 'test-domain' as Domain
function makeAccount (role: AccountRole): Account {
return {
uuid: generateId() as unknown as AccountUuid,
role,
primarySocialId: 'test-social' as PersonId,
socialIds: ['test-social' as PersonId],
fullSocialIds: []
}
}
function makeCtx (account: Account): MeasureContext<SessionData> {
const ctx = new MeasureMetricsContext('test', {}) as MeasureContext<SessionData>
ctx.contextData = {
account,
broadcast: { txes: [], queue: [], sessions: {} },
socialStringsToUsers: new Map(),
contextCache: new Map()
} as unknown as SessionData
return ctx
}
function makeMiddleware (
role: AccountRole,
opts: { provideSecurity: boolean } = { provideSecurity: false }
): { mw: SpaceSecurityMiddleware, account: Account, calls: Array<{ cls: Ref<Class<Doc>>, query: any }> } {
const account = makeAccount(role)
const calls: Array<{ cls: Ref<Class<Doc>>, query: any }> = []
const collabMixin = {
_id: generateId(),
_class: core.class.ClassCollaborators,
space: core.space.Model,
attachedTo: DOC_CLASS,
fields: ['createdBy'],
provideSecurity: opts.provideSecurity,
modifiedOn: Date.now(),
modifiedBy: core.account.System
} as unknown as ClassCollaborators<Doc>
// Partial Hierarchy test double — SpaceSecurityMiddleware only needs these methods for this test.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- full Hierarchy is not needed here
const hierarchy = {
getDomain: (_class: Ref<Class<Doc>>) => TEST_DOMAIN,
isDerived: (_class: Ref<Class<Doc>>, base: Ref<Class<Doc>>) =>
base === core.class.Space && _class === core.class.Space,
getDescendants: (_class: Ref<Class<Doc>>) => [] as Ref<Class<Doc>>[],
getAncestors: (_class: Ref<Class<Doc>>) => [_class]
} as PipelineContext['hierarchy']
const modelDb = {
findAllSync: (cl: Ref<Class<Doc>>, query: { attachedTo?: { $in?: Ref<Class<Doc>>[] } }) => {
if (cl !== core.class.ClassCollaborators) return []
const ids = query.attachedTo?.$in ?? []
return ids.includes(DOC_CLASS) ? [collabMixin] : []
}
} as unknown as PipelineContext['modelDb']
const next: Middleware = {
findAll: async <T extends Doc>(_ctx: MeasureContext<SessionData>, _class: Ref<Class<T>>, query: any) => {
calls.push({ cls: _class as unknown as Ref<Class<Doc>>, query: JSON.parse(JSON.stringify(query)) })
if (_class === (core.class.Space as unknown as Ref<Class<T>>)) {
return toFindResult([]) as any
}
if (_class === (core.class.Collaborator as unknown as Ref<Class<T>>)) {
const collabDoc: Collaborator = {
_id: generateId(),
_class: core.class.Collaborator,
space: core.space.Workspace,
attachedTo: DOC_ID,
attachedToClass: DOC_CLASS,
collection: 'collaborators',
collaborator: account.uuid,
modifiedOn: Date.now(),
modifiedBy: core.account.System
}
return toFindResult([collabDoc]) as any
}
return toFindResult([]) as any
},
tx: async () => ({}),
groupBy: async () => new Map(),
searchFulltext: async () => ({ docs: [] }) as any,
handleBroadcast: async () => {},
loadModel: async () => [],
domainRequest: async () => ({ value: undefined }) as any,
closeSession: async () => {},
close: async () => {}
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- minimal PipelineContext stub for unit test
const context = {
workspace: { uuid: generateId() as any, url: 'test', dataId: 'test' as any },
hierarchy,
modelDb,
branding: null as any,
adapterManager: {} as any,
storageAdapter: {} as any,
contextVars: {},
lastTx: '',
lastHash: '',
broadcastEvent: async () => {}
} as PipelineContext
const mw = new (SpaceSecurityMiddleware as any)(false, context, next) as SpaceSecurityMiddleware
return { mw, account, calls }
}
describe('SpaceSecurityMiddleware guest collaborator read restriction', () => {
it('applies collaborator _id filter for guest when provideSecurity is enabled', async () => {
const { mw, account, calls } = makeMiddleware(AccountRole.Guest, { provideSecurity: true })
const ctx = makeCtx(account)
await mw.findAll(ctx, DOC_CLASS, { title: 'Meeting minutes' })
expect(calls).toHaveLength(3)
expect(calls[1].cls).toBe(core.class.Collaborator)
expect(calls[2].cls).toBe(DOC_CLASS)
expect(calls[2].query).toEqual({
title: 'Meeting minutes',
_id: { $in: [DOC_ID] }
})
})
it('keeps query unchanged for regular user', async () => {
const { mw, account, calls } = makeMiddleware(AccountRole.User, { provideSecurity: true })
const ctx = makeCtx(account)
await mw.findAll(ctx, DOC_CLASS, { title: 'Meeting minutes' })
expect(calls).toHaveLength(2)
expect(calls[1].cls).toBe(DOC_CLASS)
expect(calls[1].query).toEqual({ title: 'Meeting minutes' })
})
})
+41 -1
View File
@@ -16,6 +16,7 @@
import cardPlugin, { cardId, DOMAIN_CARD, type Card, type Role } from '@hcengineering/card'
import core, {
DOMAIN_MODEL,
SortingOrder,
TxOperations,
type Class,
type ClassPermission,
@@ -71,7 +72,7 @@ export const cardOperation: MigrateOperation = {
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>, mode): Promise<void> {
await tryUpgrade(mode, state, client, cardId, [
{
state: 'migrateViewlets-v7',
state: 'migrateViewlets-6',
func: migrateViewlets
},
{
@@ -128,6 +129,11 @@ export const cardOperation: MigrateOperation = {
state: 'migrate-restricted-permissions',
mode: 'upgrade',
func: migrateRestrictedPermissions
},
{
state: 'add-grid-viewlet',
mode: 'upgrade',
func: addGridViewlet
}
])
}
@@ -286,6 +292,40 @@ async function migrateRestrictedPermissions (_client: MigrationUpgradeClient): P
}
}
async function addGridViewlet (client: MigrationUpgradeClient): Promise<void> {
const txOp = new TxOperations(client, core.account.System)
const masterTags = await client.findAll(card.class.MasterTag, {})
const currentViewlets = await client.findAll(view.class.Viewlet, {
descriptor: card.viewlet.CardGridDescriptor,
attachTo: { $in: masterTags.map((p) => p._id) }
})
for (const masterTag of masterTags) {
const current = currentViewlets.find((p) => p.attachTo === masterTag._id)
if (current === undefined) {
await txOp.createDoc(view.class.Viewlet, core.space.Model, {
descriptor: card.viewlet.CardGridDescriptor,
baseQuery: {
isLatest: true
},
config: [''],
configOptions: {
strict: true
},
viewOptions: {
groupBy: [],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['rank', SortingOrder.Ascending],
['title', SortingOrder.Descending]
],
other: []
},
attachTo: masterTag._id
})
}
}
}
async function addVersionForVersionableTypes (client: MigrationUpgradeClient): Promise<void> {
const txOp = new TxOperations(client, core.account.System)
const versionableTypes = await client.findAll(card.class.MasterTag, {})
+12
View File
@@ -461,6 +461,18 @@ export function defineFunctions (builder: Builder): void {
process.function.RoleContext
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeAny,
category: 'attribute',
label: process.string.EmptyValue,
type: 'context'
},
process.function.EmptyValue
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
+4
View File
@@ -343,6 +343,10 @@ export function createModel (builder: Builder): void {
func: serverProcess.transform.RemoveLast
})
builder.mixin(process.function.EmptyValue, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.EmptyValue
})
builder.mixin(process.function.EmptyArray, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.EmptyArray
})
@@ -13,9 +13,10 @@
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, DocumentQuery, FindOptions, Ref, WithLookup } from '@hcengineering/core'
import { type Card } from '@hcengineering/card'
import type { Class, Doc, DocumentQuery, FindOptions, Ref, WithLookup } from '@hcengineering/core'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { Scroller } from '@hcengineering/ui'
import { BuildModelKey, ViewOptions } from '@hcengineering/view'
import { ListSelectionProvider, SelectDirection, buildConfigLookup, focusStore } from '@hcengineering/view-resources'
import CardGridItem from './CardGridItem.svelte'
@@ -71,20 +72,22 @@
<ActionContext context={{ mode: 'browser' }} />
<div class="grid-container">
{#each objects as object, i}
{@const selected = selection === i}
<div class="grid-cell">
<CardGridItem
{object}
{selected}
on:obj-focus={(evt) => {
listProvider.updateFocus(evt.detail)
}}
/>
</div>
{/each}
</div>
<Scroller fade={{ multipler: { top: 3, bottom: 2.5 } }} padding={'0 1rem'} checkForHeaders>
<div class="grid-container">
{#each objects as object, i}
{@const selected = selection === i}
<div class="grid-cell">
<CardGridItem
{object}
{selected}
on:obj-focus={(evt) => {
listProvider.updateFocus(evt.detail)
}}
/>
</div>
{/each}
</div>
</Scroller>
<style lang="scss">
.grid-container {
@@ -19,7 +19,7 @@
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import { getAttributePresenterClass, getClient, hasResource } from '@hcengineering/presentation'
import { resizeObserver } from '@hcengineering/ui'
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
import view, { BuildModelKey, Viewlet } from '@hcengineering/view'
import {
buildConfigLookup,
canResolveAttribute,
@@ -34,7 +34,7 @@
const client = getClient()
const hierarchy = client.getHierarchy()
$: citems = getConfig(viewlet, undefined)
$: citems = getConfig(viewlet)
interface Config {
value: string | BuildModelKey | undefined
@@ -232,7 +232,7 @@
return false
}
function getConfig (viewlet: Viewlet, preference: ViewletPreference | undefined): Config[] {
function getConfig (viewlet: Viewlet): Config[] {
const result = getBaseConfig(viewlet)
if (viewlet.configOptions?.strict !== true) {
const allAttributes = hierarchy.getAllAttributes(viewlet.attachTo)
@@ -248,15 +248,10 @@
})
}
addAssociations(result, viewlet.attachTo, preference)
addAssociations(result, viewlet.attachTo)
}
function addAssociations (
result: Config[],
_class: Ref<Class<Doc>>,
preference: ViewletPreference | undefined,
parents: AssociationQuery[] = []
): void {
function addAssociations (result: Config[], _class: Ref<Class<Doc>>, parents: AssociationQuery[] = []): void {
const ancestors = new Set(hierarchy.getAncestors(_class))
const parent = hierarchy.getParentClass(_class)
const parentMixins = hierarchy
@@ -276,10 +271,10 @@
const associationsA = client.getModel().findAllSync(core.class.Association, { classB: { $in: allClasses } })
associationsB.forEach((a) => {
processAssociation(a, 'b', result, preference, parents)
processAssociation(a, 'b', result, parents)
})
associationsA.forEach((a) => {
processAssociation(a, 'a', result, preference, parents)
processAssociation(a, 'a', result, parents)
})
}
@@ -291,7 +286,6 @@
association: Association,
direction: 'a' | 'b',
result: Config[],
preference: ViewletPreference | undefined,
parents: AssociationQuery[]
): void {
const associationName = `$associations.${association._id}_${direction}`
@@ -327,17 +321,16 @@
result.push(newValue)
}
if (preference === undefined) return
const exists = preference.config.find((p) => {
const key = typeof p === 'string' ? p : p.key
const exists = result.find((p) => {
const key = typeof p.value === 'string' ? p.value : p.value?.key
return key === resultName
})
if (exists) {
addAssociations(result, targetClass, preference, [...parents, [association._id, direction === 'a' ? 1 : -1]])
if ((exists as AttributeConfig)?.enabled) {
addAssociations(result, targetClass, [...parents, [association._id, direction === 'a' ? 1 : -1]])
}
}
return preference === undefined ? result : []
return result
}
</script>
@@ -353,6 +346,8 @@
// TODO UBERF-9639: restore defaults
}}
on:save={(event) => {
viewlet.config = event.detail
viewlet = viewlet
dispatch('update', event.detail)
}}
/>
@@ -31,7 +31,7 @@
<span class="flex-presenter">
<span class="mr-1"><Icon {icon} size="small" /></span>
<Label label={communication.string.Unset} />
<span class="lower"><Label label={model.label} /></span>
<span class="lower ml-1"><Label label={model.label} /></span>
</span>
{:else}
<ActivityAttributeValue {model} {icon} values={value}>
+2 -1
View File
@@ -163,7 +163,8 @@
"YearFromDate": "Rok z data",
"MonthFromDate": "Měsíc z data",
"DayFromDate": "Den z data",
"DateDifference": "Rozdíl dat"
"DateDifference": "Rozdíl dat",
"EmptyValue": "Prázdná hodnota"
},
"error": {
"MethodNotFound": "Metoda nenalezena: {methodId}",
+2 -1
View File
@@ -163,7 +163,8 @@
"YearFromDate": "Jahr aus Datum",
"MonthFromDate": "Monat aus Datum",
"DayFromDate": "Tag aus Datum",
"DateDifference": "Datumsdifferenz"
"DateDifference": "Datumsdifferenz",
"EmptyValue": "Leerer Wert"
},
"error": {
"MethodNotFound": "Methode nicht gefunden: {methodId}",
+2 -1
View File
@@ -168,7 +168,8 @@
"YearFromDate": "Year from date",
"MonthFromDate": "Month from date",
"DayFromDate": "Day from date",
"DateDifference": "Date difference"
"DateDifference": "Date difference",
"EmptyValue": "Empty value"
},
"error": {
"MethodNotFound": "Method not found: {methodId}",
+2 -1
View File
@@ -168,7 +168,8 @@
"YearFromDate": "Año desde fecha",
"MonthFromDate": "Mes desde fecha",
"DayFromDate": "Día desde fecha",
"DateDifference": "Diferencia de fechas"
"DateDifference": "Diferencia de fechas",
"EmptyValue": "Valor vacío"
},
"error": {
"MethodNotFound": "Método no encontrado: {methodId}",
+2 -1
View File
@@ -168,7 +168,8 @@
"YearFromDate": "Année depuis date",
"MonthFromDate": "Mois depuis date",
"DayFromDate": "Jour depuis date",
"DateDifference": "Différence de dates"
"DateDifference": "Différence de dates",
"EmptyValue": "Valeur vide"
},
"error": {
"MethodNotFound": "Méthode introuvable : {methodId}",
+2 -1
View File
@@ -168,7 +168,8 @@
"YearFromDate": "Anno da data",
"MonthFromDate": "Mese da data",
"DayFromDate": "Giorno da data",
"DateDifference": "Differenza di date"
"DateDifference": "Differenza di date",
"EmptyValue": "Valore vuoto"
},
"error": {
"MethodNotFound": "Metodo non trovato: {methodId}",
+2 -1
View File
@@ -167,7 +167,8 @@
"YearFromDate": "日付から年",
"MonthFromDate": "日付から月",
"DayFromDate": "日付から日",
"DateDifference": "日付の差"
"DateDifference": "日付の差",
"EmptyValue": "空の値"
},
"error": {
"MethodNotFound": "メソッドが見つかりません: {methodId}",
+2 -1
View File
@@ -156,7 +156,8 @@
"YearFromDate": "Ano de data",
"MonthFromDate": "Mês de data",
"DayFromDate": "Dia de data",
"DateDifference": "Diferença de datas"
"DateDifference": "Diferença de datas",
"EmptyValue": "Valor vazio"
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
+2 -1
View File
@@ -168,7 +168,8 @@
"YearFromDate": "Ano de data",
"MonthFromDate": "Mês de data",
"DayFromDate": "Dia de data",
"DateDifference": "Diferença de datas"
"DateDifference": "Diferença de datas",
"EmptyValue": "Valor vazio"
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
+2 -1
View File
@@ -168,7 +168,8 @@
"YearFromDate": "Год из даты",
"MonthFromDate": "Месяц из даты",
"DayFromDate": "День из даты",
"DateDifference": "Разница дат"
"DateDifference": "Разница дат",
"EmptyValue": "Пустое значение"
},
"error": {
"MethodNotFound": "Метод не найден: {methodId}",
+2 -1
View File
@@ -163,7 +163,8 @@
"YearFromDate": "Tarihten yıl",
"MonthFromDate": "Tarihten ay",
"DayFromDate": "Tarihten gün",
"DateDifference": "Tarih farkı"
"DateDifference": "Tarih farkı",
"EmptyValue": "Boş değer"
},
"error": {
"MethodNotFound": "Metod bulunamadı: {methodId}",
+2 -1
View File
@@ -168,7 +168,8 @@
"YearFromDate": "日期到年份",
"MonthFromDate": "日期到月份",
"DayFromDate": "日期到天",
"DateDifference": "日期差异"
"DateDifference": "日期差异",
"EmptyValue": "空值"
},
"error": {
"MethodNotFound": "找不到方法:{methodId}",
+1
View File
@@ -233,6 +233,7 @@ export default mergeIds(processId, process, {
For: '' as IntlString,
Attribute: '' as IntlString,
Context: '' as IntlString,
EmptyValue: '' as IntlString,
EmptyArray: '' as IntlString,
ExecutionInitiator: '' as IntlString,
ExecutionStarted: '' as IntlString,
+1 -1
View File
@@ -331,7 +331,7 @@ function getContextFunctions (
break
}
default: {
if (hierarchy.isDerived(func.of, target)) {
if (hierarchy.isDerived(func.of, target) || func.of === core.class.TypeAny) {
matched.push(func._id)
}
}
+1
View File
@@ -373,6 +373,7 @@ export default plugin(processId, {
ExecutionStarted: '' as Ref<ProcessFunction>,
ExecutionEmployeeInitiator: '' as Ref<ProcessFunction>,
ExecutionInitiator: '' as Ref<ProcessFunction>,
EmptyValue: '' as Ref<ProcessFunction>,
EmptyArray: '' as Ref<ProcessFunction>,
CurrentDate: '' as Ref<ProcessFunction>,
StringFromNumber: '' as Ref<ProcessFunction>,
+1 -1
View File
@@ -21,7 +21,7 @@ import { SupportClientFactory, SupportConversation, SupportSystem } from './type
export * from './types'
export { deleteSupportConversation, updateSupportConversation } from './utils'
export const supportLink = 'https://huly.link/slack'
export const supportLink = 'https://link.huly.io/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/'
@@ -17,14 +17,14 @@
import { getEmbeddedLabel } from '@hcengineering/platform'
import { LabelAndProps, LinkWrapper, tooltip } from '@hcengineering/ui'
export let value: string | string[] | undefined
export let value: string | string[] | null | undefined
export let accent: boolean = false
export let oneLine: boolean = false
$: tooltipParams = getTooltip(value)
function getTooltip (value: string | string[] | undefined): LabelAndProps | undefined {
if (value === undefined) return
function getTooltip (value: string | string[] | null | undefined): LabelAndProps | undefined {
if (value == null) return
let str = ''
if (Array.isArray(value)) {
str = value.reduce((acc, curr, i) => (acc += i === 0 ? curr : ` ${curr}`), '')
@@ -25,6 +25,7 @@
export let loading: boolean = false
export let notify: boolean = false
export let navigator: boolean = false
export let dataId: string | undefined = undefined
</script>
<button
@@ -33,6 +34,7 @@
class:selected
class:navigator
id={'app-' + label}
data-id={dataId}
disabled={loading}
use:tooltip={{ label }}
on:click
@@ -99,6 +99,7 @@
bind:this={btns[i]}
class="ap-menuItem withIcon flex-row-center flex-grow"
class:hover={btns[i] === activeElement}
data-id={`app-switcher-row-${app.alias}`}
on:click={() => {
if (hiddenAppsIds.includes(app._id)) showApplication(app)
else hideApplication(app)
@@ -110,21 +110,28 @@
updateExcludedApps()
function isAppVisibleInSwitcher (app: Application, disabledModules: Set<Ref<Application>>): boolean {
return !hiddenAppsIds.includes(app._id) && !excludedApps.includes(app.alias) && !disabledModules.has(app._id)
let topApps: Application[] = []
let midApps: Application[] = []
let bottomApps: Application[] = []
// Single reactive block so reads of hiddenAppsIds / excludedApps / disabledApplications
$: {
const hidden = hiddenAppsIds
const excluded = excludedApps
const disabled = disabledApplications
const isApplicationVisibleInSidebar = (app: Application): boolean =>
!hidden.includes(app._id) && !excluded.includes(app.alias) && !disabled.has(app._id)
topApps = apps
.filter((it) => it.position === 'top' && isApplicationVisibleInSidebar(it))
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
midApps = apps
.filter((it) => it.position !== 'top' && it.position !== 'bottom' && isApplicationVisibleInSidebar(it))
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
bottomApps = apps.filter((it) => it.position === 'bottom' && isApplicationVisibleInSidebar(it))
}
$: topApps = apps
.filter((it) => it.position === 'top' && isAppVisibleInSwitcher(it, disabledApplications))
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
$: midApps = apps
.filter(
(it) => it.position !== 'top' && it.position !== 'bottom' && isAppVisibleInSwitcher(it, disabledApplications)
)
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
$: bottomApps = apps.filter((it) => it.position === 'bottom' && isAppVisibleInSwitcher(it, disabledApplications))
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
@@ -177,6 +184,7 @@
navigator={app._id === active && $deviceInfo.navigator.visible}
notify={showNotify(app.alias, hasInboxNotifications, hasNewInboxNotifications, hasNewMessagesNotification)}
{...customProps}
dataId={`app-sidebar-${app.alias}`}
on:click={getClickHandler(app, customProps)}
/>
</NavLink>
@@ -193,6 +201,7 @@
label={app.label}
navigator={app._id === active && $deviceInfo.navigator.visible}
{...customProps}
dataId={`app-sidebar-${app.alias}`}
on:click={getClickHandler(app, customProps)}
/>
</NavLink>
@@ -209,6 +218,7 @@
navigator={app._id === active && $deviceInfo.navigator.visible}
notify={app.alias === chatId && hasNewInboxNotifications}
{...customProps}
dataId={`app-sidebar-${app.alias}`}
on:click={getClickHandler(app, customProps)}
/>
</NavLink>
@@ -926,6 +926,7 @@
<AppItem
icon={IconSettings}
label={setting.string.Customize}
dataId="workbench-app-customize"
size={appsMini ? 'small' : 'large'}
on:click={() => showPopup(AppSwitcher, { apps }, popupPosition)}
/>
+1
View File
@@ -219,6 +219,7 @@ services:
- SECRET=secret
- STORAGE_CONFIG=${STORAGE_CONFIG}
- ACCOUNTS_URL=http://account:3003
- FRONT_URL=http://huly.local:8087
deploy:
resources:
limits:
@@ -94,6 +94,7 @@ import {
DayFromDate,
Divide,
EmptyArray,
EmptyValue,
ExecutionInitiator,
ExecutionStarted,
Filter,
@@ -680,6 +681,7 @@ export default async () => ({
RemoveFirst,
RemoveLast,
EmptyArray,
EmptyValue,
ExecutionInitiator,
ExecutionStarted,
FirstMatchValue,
@@ -463,6 +463,10 @@ export async function CurrentDate (): Promise<Timestamp> {
return Date.now()
}
export function EmptyValue (): null {
return null
}
export function EmptyArray (): any[] {
return []
}
@@ -247,12 +247,12 @@ async function getFunctionValue (
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const val = await f(res, {}, control, execution)
if (val == null) {
if (val == null && context.func !== process.function.EmptyValue) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return val
}
if (res == null) {
if (res == null && context.func !== process.function.EmptyValue) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return res
+1
View File
@@ -109,6 +109,7 @@ export default plugin(serverProcessId, {
RemoveLast: '' as Resource<TransformFunc>,
CurrentUser: '' as Resource<TransformFunc>,
CurrentDate: '' as Resource<TransformFunc>,
EmptyValue: '' as Resource<TransformFunc>,
EmptyArray: '' as Resource<TransformFunc>,
Filter: '' as Resource<TransformFunc>,
FirstMatchValue: '' as Resource<TransformFunc>,
+2
View File
@@ -58,7 +58,9 @@
"@hcengineering/server-token": "workspace:^0.7.18",
"@hcengineering/server-core": "workspace:^0.7.19",
"@hcengineering/server-storage": "workspace:^0.7.16",
"@hcengineering/api-client": "workspace:^0.7.25",
"@hcengineering/core": "workspace:^0.7.26",
"@hcengineering/server-guest-resources": "workspace:^0.7.0",
"@hcengineering/platform": "workspace:^0.7.20",
"@hcengineering/account-client": "workspace:^0.7.25",
"cors": "^2.8.5",
+2
View File
@@ -10,6 +10,7 @@ export interface Config {
Port: number
Secret: string
AccountsUrl: string
FrontUrl: string
AllowedHostnames: string[]
PuppeteerArgs: string[]
}
@@ -24,6 +25,7 @@ const config: Config = (() => {
Port: parseNumber(process.env.PORT) ?? 4005,
Secret: process.env.SECRET,
AccountsUrl: process.env.ACCOUNTS_URL,
FrontUrl: process.env.FRONT_URL,
AllowedHostnames: allowedHostnames == null ? [] : allowedHostnames.split(','),
PuppeteerArgs: puppeteerArgs.split(',')
}
+2
View File
@@ -3,6 +3,7 @@
//
import { setMetadata } from '@hcengineering/platform'
import serverCore from '@hcengineering/server-core'
import serverToken from '@hcengineering/server-token'
import { storageConfigFromEnv } from '@hcengineering/server-storage'
@@ -12,6 +13,7 @@ import { createServer, listen } from './server'
const setupMetadata = (): void => {
setMetadata(serverToken.metadata.Secret, config.Secret)
setMetadata(serverToken.metadata.Service, 'print')
setMetadata(serverCore.metadata.FrontUrl, config.FrontUrl)
}
export const main = async (): Promise<void> => {
+88 -31
View File
@@ -14,10 +14,17 @@
// limitations under the License.
//
import { generateId, newMetrics, type WorkspaceIds } from '@hcengineering/core'
import { createRestTxOperations } from '@hcengineering/api-client'
import { type Class, type Doc, generateId, newMetrics, type Ref, type WorkspaceIds } from '@hcengineering/core'
import { StorageConfiguration, initStatisticsContext } from '@hcengineering/server-core'
import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { getClient as getAccountClientRaw, AccountClient, isWorkspaceLoginInfo } from '@hcengineering/account-client'
import {
getClient as getAccountClientRaw,
AccountClient,
isWorkspaceLoginInfo,
type WorkspaceLoginInfo
} from '@hcengineering/account-client'
import { getPublicLink } from '@hcengineering/server-guest-resources'
import { createOpenTelemetryMetricsContext, SplitLogger } from '@hcengineering/analytics-service'
import cors from 'cors'
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
@@ -97,7 +104,13 @@ const extractToken = (headers: IncomingHttpHeaders, queryParams: any): string =>
}
}
type AsyncRequestHandler = (req: Request, res: Response, wsIds: WorkspaceIds, next: NextFunction) => Promise<void>
type AsyncRequestHandler = (
req: Request,
res: Response,
wsIds: WorkspaceIds,
wsLoginInfo: WorkspaceLoginInfo,
next: NextFunction
) => Promise<void>
const handleRequest = async (
fn: AsyncRequestHandler,
@@ -116,7 +129,7 @@ const handleRequest = async (
dataId: wsLoginInfo.workspaceDataId,
url: wsLoginInfo.workspaceUrl
}
await fn(req, res, wsIds, next)
await fn(req, res, wsIds, wsLoginInfo, next)
} catch (err: unknown) {
next(err)
}
@@ -127,6 +140,33 @@ const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, n
handleRequest(fn, req, res, next)
}
function parsePrintOptions (query: Request['query']): PrintOptions {
const kind = query.kind as PrintOptions['kind']
if (kind !== undefined && !validKinds.includes(kind as any)) {
throw new ApiError(400, `Invalid print kind: ${kind}`)
}
const rawWidth = (query.width ?? '') as string
const rawHeight = (query.height ?? '') as string
let viewport: PrintOptions['viewport'] | undefined
if (rawWidth.length > 0 && rawHeight.length > 0) {
viewport = {
width: parseInt(rawWidth, 10),
height: parseInt(rawHeight, 10)
}
if (Number.isNaN(viewport.width) || Number.isNaN(viewport.height)) {
throw new ApiError(400, 'Invalid width or height')
}
} else if (rawWidth.length > 0 || rawHeight.length > 0) {
throw new ApiError(400, 'Both width and height must be provided')
}
return { kind, viewport }
}
export function createServer (
storageConfig: StorageConfiguration,
allowedHostnames: string[]
@@ -154,7 +194,7 @@ export function createServer (
app.get(
'/print',
wrapRequest(async (req, res, wsIds, token) => {
wrapRequest(async (req, res, wsIds, wsLoginInfo) => {
const ctx = req.ctx
const rawlink = req.query.link as string
const link = decodeURIComponent(rawlink)
@@ -165,36 +205,15 @@ export function createServer (
!['http:', 'https:'].includes(url.protocol) ||
(whitelistedHostnames != null && !whitelistedHostnames.has(url.hostname))
) {
ctx.error('Rejected processing unexpected link', { link, token })
ctx.error('Rejected processing unexpected link', { link })
throw new ApiError(403, 'Cannot process provided link')
}
const kind = req.query.kind as PrintOptions['kind']
const options = parsePrintOptions(req.query)
if (kind !== undefined && !validKinds.includes(kind as any)) {
throw new ApiError(400, `Invalid print kind: ${kind}`)
}
const rawWidth = (req.query.width ?? '') as string
const rawHeight = (req.query.height ?? '') as string
let viewport: PrintOptions['viewport'] | undefined
if (rawWidth.length > 0 && rawHeight.length > 0) {
viewport = {
width: parseInt(rawWidth, 10),
height: parseInt(rawHeight, 10)
}
if (Number.isNaN(viewport.width) || Number.isNaN(viewport.height)) {
throw new ApiError(400, 'Invalid width or height')
}
} else if (rawWidth.length > 0 || rawHeight.length > 0) {
throw new ApiError(400, 'Both width and height must be provided')
}
const printRes = await ctx.with('print', { kind }, (ctx) => print(ctx, link, { kind, viewport }), {
const printRes = await ctx.with('print', { kind: options.kind }, (ctx) => print(ctx, link, options), {
url,
viewport
viewport: options.viewport
})
if (printRes === undefined) {
@@ -203,7 +222,7 @@ export function createServer (
const printId = `print-${generateId()}`
await storageAdapter.put(ctx, wsIds, printId, printRes, `application/${kind}`, printRes.length)
await storageAdapter.put(ctx, wsIds, printId, printRes, `application/${options.kind}`, printRes.length)
res.contentType('application/json')
res.send({ id: printId })
@@ -252,6 +271,44 @@ export function createServer (
})
)
app.get(
'/print/:objectClass/:objectId',
wrapRequest(async (req, res, wsIds, wsLoginInfo) => {
const ctx = req.ctx
const objectId = req.params.objectId
const objectClass = req.params.objectClass
const options = parsePrintOptions(req.query)
const transactorUrl = wsLoginInfo.endpoint.replace('ws://', 'http://').replace('wss://', 'https://')
const client = await createRestTxOperations(transactorUrl, wsLoginInfo.workspace, wsLoginInfo.token)
try {
const doc = await client.findOne(objectClass as Ref<Class<Doc>>, { _id: objectId as Ref<Doc> })
if (doc === undefined) {
throw new ApiError(404, 'Document not found')
}
const link = await getPublicLink(doc, client, wsIds, true, null)
const printRes = await ctx.with('print', { kind: options.kind }, (ctx) => print(ctx, link, options), {
link,
viewport: options.viewport
})
if (printRes === undefined) {
throw new ApiError(400, 'Failed to print')
}
const kind = options.kind
const contentType = kind === 'pdf' || kind === undefined ? 'application/pdf' : `image/${kind}`
res.contentType(contentType)
res.send(printRes)
} finally {
await client.close()
}
})
)
app.use((err: any, _req: any, res: any, _next: any) => {
measureCtx.error('error', { err })
@@ -0,0 +1,34 @@
import { expect, test } from '@playwright/test'
import { PlatformSetting, PlatformURI } from '../utils'
test.use({
storageState: PlatformSetting
})
test.describe('Customize sidebar applications', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${PlatformURI}/workbench/sanity-ws/recruit`)
})
test('sidebar apps match Customize visibility toggles', async ({ page }) => {
const recruitSidebar = page.getByTestId('app-sidebar-recruit')
const customize = page.getByTestId('workbench-app-customize')
const recruitRow = page.getByTestId('app-switcher-row-recruit')
await expect(recruitSidebar).toBeVisible()
await customize.click()
await expect(recruitRow).toBeVisible()
await recruitRow.click()
await page.keyboard.press('Escape')
await expect(recruitSidebar).toHaveCount(0)
await customize.click()
await expect(recruitRow).toBeVisible()
await recruitRow.click()
await page.keyboard.press('Escape')
await expect(recruitSidebar).toBeVisible()
})
})
@@ -23,7 +23,6 @@ import {
type WorkspaceToken
} from '@hcengineering/api-client'
import core, {
AccountRole,
buildSocialIdString,
concatLink,
generateId,
@@ -46,8 +45,6 @@ import { type AccountClient, getClient as getAccountClient } from '@hcengineerin
import chunter from '@hcengineering/chunter'
import contact, { ensureEmployee, type SocialIdentityRef, type Person } from '@hcengineering/contact'
import { generateToken } from '@hcengineering/server-token'
import { loveClass, LoveMeetingStatus } from '../loveApiRefs'
import WebSocket from 'ws'
describe('rest-api-server', () => {
@@ -341,104 +338,6 @@ describe('rest-api-server', () => {
})
}, 20000)
})
/**
* Requires a workspace account with AccountRole.Guest (invite / guest link) in the same workspace as `user1`.
* Set API_TESTS_GUEST_EMAIL and API_TESTS_GUEST_PASSWORD to enable.
*/
describe('guest meeting-minutes read', () => {
const guestEmail = 'guest1'
const guestPassword = '1234'
it('guest findAll does not return meeting minutes without collaborator', async () => {
const userConn = connect()
const rooms = await userConn.findAll(loveClass.Room, {}, { limit: 1 })
if (rooms.length === 0) {
throw new Error('No love Room in workspace — cannot seed MeetingMinutes')
}
const room = rooms[0]
const tx = await connectTx()
const title = `api-test-mm-${generateId()}`
const mmId = await tx.createDoc(loveClass.MeetingMinutes, core.space.Workspace, {
title,
description: null,
attachedTo: room._id,
attachedToClass: loveClass.Room,
collection: 'meetings',
status: LoveMeetingStatus.Finished
})
try {
const userFound = await userConn.findAll(loveClass.MeetingMinutes, { _id: mmId })
expect(userFound.length).toBe(1)
const guestWs = await getWorkspaceToken(
'http://huly.local:8083',
{
email: guestEmail,
password: guestPassword,
workspace: wsName
},
serverConfig
)
expect(guestWs.info.role).toBe(AccountRole.Guest)
const guestConn = createRestClient(guestWs.endpoint, guestWs.workspaceId, guestWs.token)
const guestFound = await guestConn.findAll(loveClass.MeetingMinutes, { _id: mmId })
expect(guestFound.length).toBe(0)
} finally {
await tx.removeDoc(loveClass.MeetingMinutes, core.space.Workspace, mmId)
}
}, 60000)
it('guest findAll returns meeting minutes when guest is a Collaborator', async () => {
const userConn = connect()
const rooms = await userConn.findAll(loveClass.Room, {}, { limit: 1 })
if (rooms.length === 0) {
throw new Error('No love Room in workspace — cannot seed MeetingMinutes')
}
const room = rooms[0]
const guestWs = await getWorkspaceToken(
'http://huly.local:8083',
{
email: guestEmail,
password: guestPassword,
workspace: wsName
},
serverConfig
)
expect(guestWs.info.role).toBe(AccountRole.Guest)
const tx = await connectTx()
const title = `api-test-mm-${generateId()}`
const mmId = await tx.createDoc(loveClass.MeetingMinutes, core.space.Workspace, {
title,
description: null,
attachedTo: room._id,
attachedToClass: loveClass.Room,
collection: 'meetings',
status: LoveMeetingStatus.Finished
})
const collabId = await tx.createDoc(core.class.Collaborator, core.space.Workspace, {
attachedTo: mmId,
attachedToClass: loveClass.MeetingMinutes,
collection: 'collaborators',
collaborator: guestWs.info.account
} as any)
try {
const guestConn = createRestClient(guestWs.endpoint, guestWs.workspaceId, guestWs.token)
const guestFound = await guestConn.findAll(loveClass.MeetingMinutes, { _id: mmId })
expect(guestFound.length).toBe(1)
} finally {
await tx.removeDoc(core.class.Collaborator, core.space.Workspace, collabId)
await tx.removeDoc(loveClass.MeetingMinutes, core.space.Workspace, mmId)
}
}, 60000)
})
})
async function checkFindPerformance (conn: RestClient): Promise<void> {
-17
View File
@@ -1,17 +0,0 @@
//
// Class refs for the love plugin — same id shape the model builder emits (`${pluginId}:class:${name}`).
// Use this in API tests without a dependency on `@hcengineering/love`.
//
import type { Class, Doc, Ref } from '@hcengineering/core'
export const loveClass = {
Room: 'love:class:Room' as Ref<Class<Doc>>,
MeetingMinutes: 'love:class:MeetingMinutes' as Ref<Class<Doc>>
}
/** @see `@hcengineering/love` MeetingStatus */
export enum LoveMeetingStatus {
Active = 0,
Finished = 1
}
-5
View File
@@ -32,7 +32,6 @@ echo "Creating user accounts..."
./tool.sh create-account admin -f Super -l Admin -p 1234
./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 guest1 -f Guest -l One -p 1234
echo "Creating workspace api-tests..."
./tool.sh create-workspace api-tests email:user1
@@ -44,8 +43,4 @@ echo "Assigning user1 to workspaces..."
./tool.sh assign-workspace user1 api-tests
./tool.sh assign-workspace user1 api-tests-cr
echo "Assigning guest1 to api-tests as Guest..."
./tool.sh assign-workspace guest1 api-tests --role Guest
./tool.sh assign-workspace guest1 api-tests-cr --role Guest
rm -rf ./sanity/.auth