mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBER-937: Extensibility changes (#3874)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
1fd913a2b5
commit
e09bd25a87
@ -113,7 +113,7 @@ It may also be necessary to upgrade the running database.
|
||||
|
||||
```bash
|
||||
cd ./dev/tool
|
||||
rushx upgrade
|
||||
rushx upgrade -f
|
||||
```
|
||||
|
||||
In cases where the project fails to build for any logical reason, try the following steps:
|
||||
|
@ -124,7 +124,8 @@ services:
|
||||
- SERVER_SECRET=secret
|
||||
- ELASTIC_URL=http://elastic:9200
|
||||
- MONGO_URL=mongodb://mongodb:27017
|
||||
- METRICS_CONSOLE=true
|
||||
- METRICS_CONSOLE=false
|
||||
- METRICS_FILE=metrics.txt
|
||||
- MINIO_ENDPOINT=minio
|
||||
- MINIO_ACCESS_KEY=minioadmin
|
||||
- MINIO_SECRET_KEY=minioadmin
|
||||
|
@ -50,7 +50,7 @@ import { diffWorkspace } from './workspace'
|
||||
|
||||
import { Data, getWorkspaceId, RateLimitter, Tx, Version } from '@hcengineering/core'
|
||||
import { MinioService } from '@hcengineering/minio'
|
||||
import { MigrateOperation } from '@hcengineering/model'
|
||||
import { consoleModelLogger, MigrateOperation } from '@hcengineering/model'
|
||||
import { openAIConfigDefaults } from '@hcengineering/openai'
|
||||
import path from 'path'
|
||||
import { benchmark } from './benchmark'
|
||||
@ -237,8 +237,13 @@ export function devTool (
|
||||
.option('-p|--parallel <parallel>', 'Parallel upgrade', '0')
|
||||
.option('-l|--logs <logs>', 'Default logs folder', './logs')
|
||||
.option('-r|--retry <retry>', 'Number of apply retries', '0')
|
||||
.option(
|
||||
'-c|--console',
|
||||
'Display all information into console(default will create logs folder with {workspace}.log files',
|
||||
false
|
||||
)
|
||||
.option('-f|--force [force]', 'Force update', false)
|
||||
.action(async (cmd: { parallel: string, logs: string, retry: string, force: boolean }) => {
|
||||
.action(async (cmd: { parallel: string, logs: string, retry: string, force: boolean, console: boolean }) => {
|
||||
const { mongodbUri, version, txes, migrateOperations } = prepareTools()
|
||||
return await withDatabase(mongodbUri, async (db) => {
|
||||
const workspaces = await listWorkspaces(db, productId)
|
||||
@ -246,8 +251,10 @@ export function devTool (
|
||||
|
||||
async function _upgradeWorkspace (ws: WorkspaceInfoOnly): Promise<void> {
|
||||
const t = Date.now()
|
||||
const logger = new FileModelLogger(path.join(cmd.logs, `${ws.workspace}.log`))
|
||||
console.log('---UPGRADING----', ws.workspace, logger.file)
|
||||
const logger = cmd.console
|
||||
? consoleModelLogger
|
||||
: new FileModelLogger(path.join(cmd.logs, `${ws.workspace}.log`))
|
||||
console.log('---UPGRADING----', ws.workspace, !cmd.console ? (logger as FileModelLogger).file : '')
|
||||
try {
|
||||
await upgradeWorkspace(version, txes, migrateOperations, productId, db, ws.workspace, logger, cmd.force)
|
||||
console.log('---UPGRADING-DONE----', ws.workspace, Date.now() - t)
|
||||
@ -256,7 +263,9 @@ export function devTool (
|
||||
logger.log('error', JSON.stringify(err))
|
||||
console.log('---UPGRADING-FAILED----', ws.workspace, Date.now() - t)
|
||||
} finally {
|
||||
logger.close()
|
||||
if (!cmd.console) {
|
||||
;(logger as FileModelLogger).close()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cmd.parallel !== '0') {
|
||||
|
@ -1,11 +1,17 @@
|
||||
//
|
||||
|
||||
import { Class, DOMAIN_TX, Doc, Domain, Ref, TxOperations } from '@hcengineering/core'
|
||||
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
||||
import {
|
||||
MigrateOperation,
|
||||
MigrationClient,
|
||||
MigrationUpgradeClient,
|
||||
ModelLogger,
|
||||
tryMigrate
|
||||
} from '@hcengineering/model'
|
||||
import { DOMAIN_COMMENT } from '@hcengineering/model-chunter'
|
||||
import core from '@hcengineering/model-core'
|
||||
import { DOMAIN_VIEW } from '@hcengineering/model-view'
|
||||
import contact, { DOMAIN_CONTACT } from './index'
|
||||
import contact, { DOMAIN_CONTACT, contactId } from './index'
|
||||
|
||||
async function createSpace (tx: TxOperations): Promise<void> {
|
||||
const current = await tx.findOne(core.class.Space, {
|
||||
@ -76,118 +82,125 @@ async function createEmployeeEmail (client: TxOperations): Promise<void> {
|
||||
}
|
||||
|
||||
export const contactOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> {
|
||||
await tryMigrate(client, contactId, [
|
||||
{
|
||||
objectClass: 'contact:class:EmployeeAccount'
|
||||
},
|
||||
{
|
||||
$rename: { 'attributes.employee': 'attributes.person' },
|
||||
$set: { objectClass: contact.class.PersonAccount }
|
||||
}
|
||||
)
|
||||
state: 'employees',
|
||||
func: async (client) => {
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
objectClass: 'contact:class:EmployeeAccount'
|
||||
},
|
||||
{
|
||||
$rename: { 'attributes.employee': 'attributes.person' },
|
||||
$set: { objectClass: contact.class.PersonAccount }
|
||||
}
|
||||
)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
objectClass: 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { objectClass: contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
objectClass: 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { objectClass: contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
'tx.attributes.backlinkClass': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'tx.attributes.backlinkClass': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
'tx.attributes.backlinkClass': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'tx.attributes.backlinkClass': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
'tx.attributes.backlinkClass': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'tx.attributes.backlinkClass': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
'tx.attributes.backlinkClass': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'tx.attributes.backlinkClass': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
objectClass: core.class.Attribute,
|
||||
'attributes.type.to': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'attributes.type.to': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
objectClass: core.class.Attribute,
|
||||
'operations.type.to': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'operations.type.to': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
objectClass: core.class.Attribute,
|
||||
'attributes.type.to': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'attributes.type.to': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
objectClass: core.class.Attribute,
|
||||
'operations.type.to': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'operations.type.to': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
'attributes.extends': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'attributes.extends': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
'attributes.extends': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'attributes.extends': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
|
||||
for (const d of client.hierarchy.domains()) {
|
||||
await client.update(
|
||||
d,
|
||||
{ attachedToClass: 'contact:class:Employee' },
|
||||
{ $set: { attachedToClass: contact.mixin.Employee } }
|
||||
)
|
||||
}
|
||||
await client.update(
|
||||
DOMAIN_COMMENT,
|
||||
{ backlinkClass: 'contact:class:Employee' },
|
||||
{ $set: { backlinkClass: contact.mixin.Employee } }
|
||||
)
|
||||
await client.update(
|
||||
'tags' as Domain,
|
||||
{ targetClass: 'contact:class:Employee' },
|
||||
{ $set: { targetClass: contact.mixin.Employee } }
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_VIEW,
|
||||
{ filterClass: 'contact:class:Employee' },
|
||||
{ $set: { filterClass: contact.mixin.Employee } }
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_CONTACT,
|
||||
{
|
||||
_class: 'contact:class:Employee' as Ref<Class<Doc>>
|
||||
},
|
||||
{
|
||||
$rename: {
|
||||
active: `${contact.mixin.Employee as string}.active`,
|
||||
statuses: `${contact.mixin.Employee as string}.statuses`,
|
||||
displayName: `${contact.mixin.Employee as string}.displayName`,
|
||||
position: `${contact.mixin.Employee as string}.position`
|
||||
},
|
||||
$set: {
|
||||
_class: contact.class.Person
|
||||
for (const d of client.hierarchy.domains()) {
|
||||
await client.update(
|
||||
d,
|
||||
{ attachedToClass: 'contact:class:Employee' },
|
||||
{ $set: { attachedToClass: contact.mixin.Employee } }
|
||||
)
|
||||
}
|
||||
await client.update(
|
||||
DOMAIN_COMMENT,
|
||||
{ backlinkClass: 'contact:class:Employee' },
|
||||
{ $set: { backlinkClass: contact.mixin.Employee } }
|
||||
)
|
||||
await client.update(
|
||||
'tags' as Domain,
|
||||
{ targetClass: 'contact:class:Employee' },
|
||||
{ $set: { targetClass: contact.mixin.Employee } }
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_VIEW,
|
||||
{ filterClass: 'contact:class:Employee' },
|
||||
{ $set: { filterClass: contact.mixin.Employee } }
|
||||
)
|
||||
await client.update(
|
||||
DOMAIN_CONTACT,
|
||||
{
|
||||
_class: 'contact:class:Employee' as Ref<Class<Doc>>
|
||||
},
|
||||
{
|
||||
$rename: {
|
||||
active: `${contact.mixin.Employee as string}.active`,
|
||||
statuses: `${contact.mixin.Employee as string}.statuses`,
|
||||
displayName: `${contact.mixin.Employee as string}.displayName`,
|
||||
position: `${contact.mixin.Employee as string}.position`
|
||||
},
|
||||
$set: {
|
||||
_class: contact.class.Person
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
])
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
|
@ -13,23 +13,28 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { DOMAIN_MODEL } from '@hcengineering/core'
|
||||
import { Builder, Model } from '@hcengineering/model'
|
||||
import { Class, DOMAIN_MODEL, Doc, Ref } from '@hcengineering/core'
|
||||
import { Builder, Model, Prop, TypeRef } from '@hcengineering/model'
|
||||
import core, { TDoc } from '@hcengineering/model-core'
|
||||
import { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
// Import types to prevent .svelte components to being exposed to type typescript.
|
||||
import { PresentationMiddlewareCreator, PresentationMiddlewareFactory } from '@hcengineering/presentation'
|
||||
import {
|
||||
ComponentPointExtension,
|
||||
CreateExtensionKind,
|
||||
DocAttributeRule,
|
||||
DocRules,
|
||||
DocCreateExtension,
|
||||
DocCreateFunction,
|
||||
ObjectSearchCategory,
|
||||
ObjectSearchFactory
|
||||
} from '@hcengineering/presentation/src/types'
|
||||
import presentation from './plugin'
|
||||
import { PresentationMiddlewareCreator, PresentationMiddlewareFactory } from '@hcengineering/presentation'
|
||||
import { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
|
||||
import presentation from './plugin'
|
||||
|
||||
export { presentationId } from '@hcengineering/presentation/src/plugin'
|
||||
export { default } from './plugin'
|
||||
export { ObjectSearchCategory, ObjectSearchFactory }
|
||||
export { CreateExtensionKind, DocCreateExtension, DocCreateFunction, ObjectSearchCategory, ObjectSearchFactory }
|
||||
|
||||
@Model(presentation.class.ObjectSearchCategory, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TObjectSearchCategory extends TDoc implements ObjectSearchCategory {
|
||||
@ -53,6 +58,29 @@ export class TComponentPointExtension extends TDoc implements ComponentPointExte
|
||||
order!: number
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TObjectSearchCategory, TPresentationMiddlewareFactory, TComponentPointExtension)
|
||||
@Model(presentation.class.DocCreateExtension, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TDocCreateExtension extends TDoc implements DocCreateExtension {
|
||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||
ofClass!: Ref<Class<Doc>>
|
||||
|
||||
components!: Record<CreateExtensionKind, AnyComponent>
|
||||
apply!: Resource<DocCreateFunction>
|
||||
}
|
||||
|
||||
@Model(presentation.class.DocRules, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TDocRules extends TDoc implements DocRules {
|
||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||
ofClass!: Ref<Class<Doc>>
|
||||
|
||||
fieldRules!: DocAttributeRule[]
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TObjectSearchCategory,
|
||||
TPresentationMiddlewareFactory,
|
||||
TComponentPointExtension,
|
||||
TDocCreateExtension,
|
||||
TDocRules
|
||||
)
|
||||
}
|
||||
|
@ -35,7 +35,15 @@ export const recruitOperation: MigrateOperation = {
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
await createDefaults(tx)
|
||||
await fixTemplateSpace(tx)
|
||||
|
||||
await tryUpgrade(client, recruitId, [
|
||||
{
|
||||
state: 'fix-template-space',
|
||||
func: async (client) => {
|
||||
await fixTemplateSpace(tx)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
await tryUpgrade(client, recruitId, [
|
||||
{
|
||||
|
@ -419,13 +419,15 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: view.actionImpl.ValueSelector,
|
||||
actionPopup: view.component.ValueSelector,
|
||||
action: view.actionImpl.AttributeSelector,
|
||||
actionPopup: tracker.component.AssigneeEditor,
|
||||
actionProps: {
|
||||
attribute: 'assignee',
|
||||
_class: contact.mixin.Employee,
|
||||
query: {},
|
||||
placeholder: tracker.string.AssignTo
|
||||
isAction: true,
|
||||
valueKey: 'object'
|
||||
// _class: contact.mixin.Employee,
|
||||
// query: {},
|
||||
// placeholder: tracker.string.AssignTo
|
||||
},
|
||||
label: tracker.string.Assignee,
|
||||
icon: contact.icon.Person,
|
||||
@ -445,16 +447,11 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: view.actionImpl.ValueSelector,
|
||||
actionPopup: view.component.ValueSelector,
|
||||
action: view.actionImpl.AttributeSelector,
|
||||
actionPopup: tracker.component.ComponentEditor,
|
||||
actionProps: {
|
||||
attribute: 'component',
|
||||
_class: tracker.class.Component,
|
||||
query: {},
|
||||
fillQuery: { space: 'space' },
|
||||
docMatches: ['space'],
|
||||
searchField: 'label',
|
||||
placeholder: tracker.string.Component
|
||||
isAction: true
|
||||
},
|
||||
label: tracker.string.Component,
|
||||
icon: tracker.icon.Component,
|
||||
|
@ -36,7 +36,6 @@ import {
|
||||
TIssueTemplate,
|
||||
TMilestone,
|
||||
TProject,
|
||||
TProjectIssueTargetOptions,
|
||||
TRelatedIssueTarget,
|
||||
TTimeSpendReport,
|
||||
TTypeEstimation,
|
||||
@ -431,7 +430,6 @@ export function createModel (builder: Builder): void {
|
||||
TTypeMilestoneStatus,
|
||||
TTimeSpendReport,
|
||||
TTypeReportedTime,
|
||||
TProjectIssueTargetOptions,
|
||||
TRelatedIssueTarget,
|
||||
TTypeEstimation,
|
||||
TTypeRemainingTime
|
||||
|
@ -30,7 +30,6 @@ import {
|
||||
Collection,
|
||||
Hidden,
|
||||
Index,
|
||||
Mixin,
|
||||
Model,
|
||||
Prop,
|
||||
ReadOnly,
|
||||
@ -43,9 +42,9 @@ import {
|
||||
} from '@hcengineering/model'
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import core, { TAttachedDoc, TClass, TDoc, TStatus, TType } from '@hcengineering/model-core'
|
||||
import core, { TAttachedDoc, TDoc, TStatus, TType } from '@hcengineering/model-core'
|
||||
import task, { TSpaceWithStates, TTask } from '@hcengineering/model-task'
|
||||
import { IntlString, Resource } from '@hcengineering/platform'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import tags, { TagElement } from '@hcengineering/tags'
|
||||
import { DoneState } from '@hcengineering/task'
|
||||
import {
|
||||
@ -57,18 +56,15 @@ import {
|
||||
IssueStatus,
|
||||
IssueTemplate,
|
||||
IssueTemplateChild,
|
||||
IssueUpdateFunction,
|
||||
Milestone,
|
||||
MilestoneStatus,
|
||||
Project,
|
||||
ProjectIssueTargetOptions,
|
||||
RelatedClassRule,
|
||||
RelatedIssueTarget,
|
||||
RelatedSpaceRule,
|
||||
TimeReportDayType,
|
||||
TimeSpendReport
|
||||
} from '@hcengineering/tracker'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import tracker from './plugin'
|
||||
|
||||
export const DOMAIN_TRACKER = 'tracker' as Domain
|
||||
@ -357,14 +353,6 @@ export class TComponent extends TDoc implements Component {
|
||||
declare space: Ref<Project>
|
||||
}
|
||||
|
||||
@Mixin(tracker.mixin.ProjectIssueTargetOptions, core.class.Class)
|
||||
export class TProjectIssueTargetOptions extends TClass implements ProjectIssueTargetOptions {
|
||||
headerComponent!: AnyComponent
|
||||
bodyComponent!: AnyComponent
|
||||
footerComponent!: AnyComponent
|
||||
|
||||
update!: Resource<IssueUpdateFunction>
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -483,7 +483,8 @@ export function defineViewlets (builder: Builder): void {
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.ComponentPresenter,
|
||||
props: { kind: 'list' }
|
||||
props: { kind: 'list' },
|
||||
displayProps: { key: 'component', fixed: 'left' }
|
||||
},
|
||||
{ key: '', displayProps: { grow: true } },
|
||||
{
|
||||
|
@ -110,6 +110,26 @@ export class Hierarchy {
|
||||
}
|
||||
}
|
||||
|
||||
findMixinMixins<D extends Doc, M extends D>(doc: Doc, mixin: Ref<Mixin<M>>): M[] {
|
||||
const _doc = _toDoc(doc)
|
||||
const result: M[] = []
|
||||
const resultSet = new Set<string>()
|
||||
// Find all potential mixins of doc
|
||||
for (const [k, v] of Object.entries(_doc)) {
|
||||
if (typeof v === 'object' && this.classifiers.has(k as Ref<Classifier>)) {
|
||||
const clazz = this.getClass(k as Ref<Classifier>)
|
||||
if (this.hasMixin(clazz, mixin)) {
|
||||
const cc = this.as(clazz, mixin) as any as M
|
||||
if (cc !== undefined && !resultSet.has(cc._id)) {
|
||||
result.push(cc)
|
||||
resultSet.add(cc._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
isMixin (_class: Ref<Class<Doc>>): boolean {
|
||||
const data = this.classifiers.get(_class)
|
||||
return data !== undefined && this._isMixin(data)
|
||||
|
@ -14,11 +14,11 @@
|
||||
//
|
||||
|
||||
import { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import { getObjectValue, Lookup, ReverseLookups } from '.'
|
||||
import { Lookup, ReverseLookups, getObjectValue } from '.'
|
||||
import type { Class, Doc, Ref } from './classes'
|
||||
import core from './component'
|
||||
import { Hierarchy } from './hierarchy'
|
||||
import { matchQuery, resultSort, checkMixinKey } from './query'
|
||||
import { checkMixinKey, matchQuery, resultSort } from './query'
|
||||
import type { DocumentQuery, FindOptions, FindResult, LookupData, Storage, TxResult, WithLookup } from './storage'
|
||||
import type { Tx, TxCreateDoc, TxMixin, TxRemoveDoc, TxUpdateDoc } from './tx'
|
||||
import { TxProcessor } from './tx'
|
||||
@ -175,6 +175,34 @@ export abstract class MemDb extends TxProcessor implements Storage {
|
||||
return toFindResult(res, total)
|
||||
}
|
||||
|
||||
/**
|
||||
* Only in model find without lookups and sorting.
|
||||
*/
|
||||
findAllSync<T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>): FindResult<T> {
|
||||
let result: WithLookup<Doc>[]
|
||||
const baseClass = this.hierarchy.getBaseClass(_class)
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(query, '_id') &&
|
||||
(typeof query._id === 'string' || query._id?.$in !== undefined || query._id === undefined || query._id === null)
|
||||
) {
|
||||
result = this.getByIdQuery(query, baseClass)
|
||||
} else {
|
||||
result = this.getObjectsByClass(baseClass)
|
||||
}
|
||||
|
||||
result = matchQuery(result, query, _class, this.hierarchy, true)
|
||||
|
||||
if (baseClass !== _class) {
|
||||
// We need to filter instances without mixin was set
|
||||
result = result.filter((r) => (r as any)[_class] !== undefined)
|
||||
}
|
||||
const total = result.length
|
||||
result = result.slice(0, options?.limit)
|
||||
const tresult = this.hierarchy.clone(result) as WithLookup<T>[]
|
||||
const res = tresult.map((it) => this.hierarchy.updateLookupMixin(_class, it, options))
|
||||
return toFindResult(res, total)
|
||||
}
|
||||
|
||||
addDoc (doc: Doc): void {
|
||||
this.hierarchy.getAncestors(doc._class).forEach((_class) => {
|
||||
const arr = this.getObjectsByClass(_class)
|
||||
|
@ -325,8 +325,6 @@
|
||||
<KanbanRow
|
||||
bind:this={stateRows[si]}
|
||||
on:obj-focus
|
||||
index={si}
|
||||
{groupByDocs}
|
||||
{stateObjects}
|
||||
{isDragging}
|
||||
{dragCard}
|
||||
|
@ -18,17 +18,14 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { CardDragEvent, DocWithRank, Item } from '../types'
|
||||
import Spinner from '@hcengineering/ui/src/components/Spinner.svelte'
|
||||
|
||||
export let stateObjects: Item[]
|
||||
export let isDragging: boolean
|
||||
export let dragCard: Item | undefined
|
||||
export let objects: Item[]
|
||||
export let groupByDocs: Record<string | number, Item[]>
|
||||
export let selection: number | undefined = undefined
|
||||
export let checkedSet: Set<Ref<Doc>>
|
||||
export let state: CategoryType
|
||||
export let index: number
|
||||
|
||||
export let cardDragOver: (evt: CardDragEvent, object: Item) => void
|
||||
export let cardDrop: (evt: CardDragEvent, object: Item) => void
|
||||
@ -56,23 +53,7 @@
|
||||
let limit = 50
|
||||
|
||||
let limitedObjects: DocWithRank[] = []
|
||||
let loading = false
|
||||
let loadingTimeout: any | undefined = undefined
|
||||
|
||||
function update (stateObjects: Item[], limit: number | undefined, index: number): void {
|
||||
clearTimeout(loadingTimeout)
|
||||
if (limitedObjects.length > 0 || index === 0) {
|
||||
limitedObjects = stateObjects.slice(0, limit)
|
||||
} else {
|
||||
loading = true
|
||||
loadingTimeout = setTimeout(() => {
|
||||
limitedObjects = stateObjects.slice(0, limit)
|
||||
loading = false
|
||||
}, index)
|
||||
}
|
||||
}
|
||||
|
||||
$: update(stateObjects, limit, index)
|
||||
$: limitedObjects = stateObjects.slice(0, limit)
|
||||
</script>
|
||||
|
||||
{#each limitedObjects as object, i (object._id)}
|
||||
@ -110,21 +91,17 @@
|
||||
{/each}
|
||||
{#if stateObjects.length > limitedObjects.length}
|
||||
<div class="p-1 flex-no-shrink clear-mins">
|
||||
{#if loading}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<div class="card-container flex-between p-4">
|
||||
<span class="caption-color">{limitedObjects.length}</span> / {stateObjects.length}
|
||||
<Button
|
||||
size={'small'}
|
||||
icon={IconMoreH}
|
||||
label={ui.string.ShowMore}
|
||||
on:click={() => {
|
||||
limit = limit + 20
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-container flex-between p-4">
|
||||
<span class="caption-color">{limitedObjects.length}</span> / {stateObjects.length}
|
||||
<Button
|
||||
size={'small'}
|
||||
icon={IconMoreH}
|
||||
label={ui.string.ShowMore}
|
||||
on:click={() => {
|
||||
limit = limit + 20
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@ -133,11 +110,7 @@
|
||||
background-color: var(--theme-kanban-card-bg-color);
|
||||
border: 1px solid var(--theme-kanban-card-border);
|
||||
border-radius: 0.25rem;
|
||||
// transition: box-shadow .15s ease-in-out;
|
||||
|
||||
// &:hover {
|
||||
// background-color: var(--highlight-hover);
|
||||
// }
|
||||
&.checked {
|
||||
background-color: var(--highlight-select);
|
||||
box-shadow: 0 0 1px 1px var(--highlight-select-border);
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Component, ComponentExtensionId } from '@hcengineering/ui'
|
||||
import plugin from '../plugin'
|
||||
import { ComponentPointExtension } from '../types'
|
||||
import { getClient } from '../utils'
|
||||
import plugin from '../../plugin'
|
||||
import { ComponentPointExtension } from '../../types'
|
||||
import { getClient } from '../../utils'
|
||||
|
||||
export let extension: ComponentExtensionId
|
||||
export let props: Record<string, any> = {}
|
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { CreateExtensionKind } from '../../types'
|
||||
import { DocCreateExtensionManager } from './manager'
|
||||
import { Space } from '@hcengineering/core'
|
||||
|
||||
export let manager: DocCreateExtensionManager
|
||||
export let kind: CreateExtensionKind
|
||||
export let props: Record<string, any> = {}
|
||||
export let space: Space | undefined
|
||||
|
||||
$: extensions = manager.extensions
|
||||
|
||||
$: filteredExtensions = $extensions.filter((it) => it.components[kind] !== undefined)
|
||||
</script>
|
||||
|
||||
{#each filteredExtensions as extension}
|
||||
{@const state = manager.getState(extension._id)}
|
||||
{@const component = extension.components[kind]}
|
||||
{#if component}
|
||||
<Component is={component} props={{ kind, state, space, ...props }} />
|
||||
{/if}
|
||||
{/each}
|
56
packages/presentation/src/components/extensions/manager.ts
Normal file
56
packages/presentation/src/components/extensions/manager.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Class, Doc, DocData, Ref, SortingOrder, Space, TxOperations } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { Writable, writable } from 'svelte/store'
|
||||
import { LiveQuery } from '../..'
|
||||
import presentation from '../../plugin'
|
||||
import { DocCreateExtension } from '../../types'
|
||||
import { createQuery } from '../../utils'
|
||||
|
||||
export class DocCreateExtensionManager {
|
||||
query: LiveQuery
|
||||
_extensions: DocCreateExtension[] = []
|
||||
extensions: Writable<DocCreateExtension[]> = writable([])
|
||||
states: Map<Ref<DocCreateExtension>, Writable<any>> = new Map()
|
||||
|
||||
static create (_class: Ref<Class<Doc>>): DocCreateExtensionManager {
|
||||
const mgr = new DocCreateExtensionManager(_class)
|
||||
onDestroy(() => {
|
||||
mgr.close()
|
||||
})
|
||||
return mgr
|
||||
}
|
||||
|
||||
getState (ref: Ref<DocCreateExtension>): Writable<any> {
|
||||
let state = this.states.get(ref)
|
||||
if (state === undefined) {
|
||||
state = writable({})
|
||||
this.states.set(ref, state)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
private constructor (readonly _class: Ref<Class<Doc>>) {
|
||||
this.query = createQuery()
|
||||
this.query.query(
|
||||
presentation.class.DocCreateExtension,
|
||||
{ ofClass: _class },
|
||||
(res) => {
|
||||
this._extensions = res
|
||||
this.extensions.set(res)
|
||||
},
|
||||
{ sort: { ofClass: SortingOrder.Ascending } }
|
||||
)
|
||||
}
|
||||
|
||||
async commit (ops: TxOperations, docId: Ref<Doc>, space: Ref<Space>, data: DocData<Doc>): Promise<void> {
|
||||
for (const e of this._extensions) {
|
||||
const applyOp = await getResource(e.apply)
|
||||
await applyOp?.(ops, docId, space, data, this.getState(e._id))
|
||||
}
|
||||
}
|
||||
|
||||
close (): void {
|
||||
this.query.unsubscribe()
|
||||
}
|
||||
}
|
@ -41,7 +41,8 @@ export { default as NavLink } from './components/NavLink.svelte'
|
||||
export { default as IconForward } from './components/icons/Forward.svelte'
|
||||
export { default as Breadcrumbs } from './components/breadcrumbs/Breadcrumbs.svelte'
|
||||
export { default as BreadcrumbsElement } from './components/breadcrumbs/BreadcrumbsElement.svelte'
|
||||
export { default as ComponentExtensions } from './components/ComponentExtensions.svelte'
|
||||
export { default as ComponentExtensions } from './components/extensions/ComponentExtensions.svelte'
|
||||
export { default as DocCreateExtComponent } from './components/extensions/DocCreateExtComponent.svelte'
|
||||
export { default } from './plugin'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
@ -50,3 +51,5 @@ export { presentationId }
|
||||
export * from './configuration'
|
||||
export * from './context'
|
||||
export * from './pipeline'
|
||||
export * from './components/extensions/manager'
|
||||
export * from './rules'
|
||||
|
@ -10,7 +10,8 @@ import {
|
||||
Ref,
|
||||
Tx,
|
||||
TxResult,
|
||||
WithLookup
|
||||
WithLookup,
|
||||
toFindResult
|
||||
} from '@hcengineering/core'
|
||||
import { Resource } from '@hcengineering/platform'
|
||||
|
||||
@ -240,3 +241,53 @@ export abstract class BasePresentationMiddleware {
|
||||
export interface PresentationMiddlewareFactory extends Doc {
|
||||
createPresentationMiddleware: Resource<PresentationMiddlewareCreator>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class OptimizeQueryMiddleware extends BasePresentationMiddleware implements PresentationMiddleware {
|
||||
private constructor (client: Client, next?: PresentationMiddleware) {
|
||||
super(client, next)
|
||||
}
|
||||
|
||||
static create (client: Client, next?: PresentationMiddleware): OptimizeQueryMiddleware {
|
||||
return new OptimizeQueryMiddleware(client, next)
|
||||
}
|
||||
|
||||
async notifyTx (tx: Tx): Promise<void> {
|
||||
await this.provideNotifyTx(tx)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
return await this.provideClose()
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
return await this.provideTx(tx)
|
||||
}
|
||||
|
||||
async subscribe<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options: FindOptions<T> | undefined,
|
||||
refresh: () => void
|
||||
): Promise<{
|
||||
unsubscribe: () => void
|
||||
query?: DocumentQuery<T>
|
||||
options?: FindOptions<T>
|
||||
}> {
|
||||
return await this.provideSubscribe(_class, query, options, refresh)
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T> | undefined
|
||||
): Promise<FindResult<T>> {
|
||||
if (_class == null || typeof query !== 'object' || ('_class' in query && query._class == null)) {
|
||||
console.error('_class must be specified in query', query)
|
||||
return toFindResult([], 0)
|
||||
}
|
||||
return await this.provideFindAll(_class, query, options)
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import { Class, Ref } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import { PresentationMiddlewareFactory } from './pipeline'
|
||||
import { ComponentPointExtension, ObjectSearchCategory } from './types'
|
||||
import { ComponentPointExtension, DocRules, DocCreateExtension, ObjectSearchCategory } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -29,7 +29,9 @@ export default plugin(presentationId, {
|
||||
class: {
|
||||
ObjectSearchCategory: '' as Ref<Class<ObjectSearchCategory>>,
|
||||
PresentationMiddlewareFactory: '' as Ref<Class<PresentationMiddlewareFactory>>,
|
||||
ComponentPointExtension: '' as Ref<Class<ComponentPointExtension>>
|
||||
ComponentPointExtension: '' as Ref<Class<ComponentPointExtension>>,
|
||||
DocCreateExtension: '' as Ref<Class<DocCreateExtension>>,
|
||||
DocRules: '' as Ref<Class<DocRules>>
|
||||
},
|
||||
string: {
|
||||
Create: '' as IntlString,
|
||||
|
108
packages/presentation/src/rules.ts
Normal file
108
packages/presentation/src/rules.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { Class, Doc, DocumentQuery, Ref, Space, matchQuery } from '@hcengineering/core'
|
||||
import { getClient } from '.'
|
||||
import presentation from './plugin'
|
||||
|
||||
export interface RuleApplyResult<T extends Doc> {
|
||||
fieldQuery: DocumentQuery<T>
|
||||
disableUnset: boolean
|
||||
disableEdit: boolean
|
||||
}
|
||||
|
||||
export const emptyRuleApplyResult: RuleApplyResult<Doc> = {
|
||||
fieldQuery: {},
|
||||
disableUnset: false,
|
||||
disableEdit: false
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getDocRules<T extends Doc> (documents: Doc | Doc[], field: string): RuleApplyResult<T> | undefined {
|
||||
const docs = Array.isArray(documents) ? documents : [documents]
|
||||
if (docs.length === 0) {
|
||||
return emptyRuleApplyResult as RuleApplyResult<T>
|
||||
}
|
||||
const c = getClient()
|
||||
const h = c.getHierarchy()
|
||||
|
||||
const _class = docs[0]._class
|
||||
for (const d of docs) {
|
||||
if (d._class !== _class) {
|
||||
// If we have different classes, we should return undefined.
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const rulesSet = c.getModel().findAllSync(presentation.class.DocRules, { ofClass: { $in: h.getAncestors(_class) } })
|
||||
let fieldQuery: DocumentQuery<T> = {}
|
||||
let disableUnset = false
|
||||
let disableEdit = false
|
||||
for (const rules of rulesSet) {
|
||||
if (h.isDerived(_class, rules.ofClass)) {
|
||||
// Check individual rules and form a result query
|
||||
for (const r of rules.fieldRules) {
|
||||
if (r.field === field) {
|
||||
const _docs = docs.map((doc) =>
|
||||
r.mixin !== undefined && h.hasMixin(doc, r.mixin) ? h.as(doc, r.mixin) : doc
|
||||
)
|
||||
if (matchQuery(_docs, r.query, r.mixin ?? rules.ofClass, h).length === _docs.length) {
|
||||
// We have rule match.
|
||||
if (r.disableUnset === true) {
|
||||
disableUnset = true
|
||||
}
|
||||
if (r.disableEdit === true) {
|
||||
disableEdit = true
|
||||
}
|
||||
if (r.fieldQuery != null) {
|
||||
fieldQuery = { ...fieldQuery, ...r.fieldQuery }
|
||||
}
|
||||
|
||||
for (const [sourceK, targetK] of Object.entries(r.fieldQueryFill ?? {})) {
|
||||
const v = (_docs[0] as any)[sourceK]
|
||||
for (const d of _docs) {
|
||||
const newV = (d as any)[sourceK]
|
||||
if (newV !== v && r.allowConflict === false) {
|
||||
// Value conflict, we could not choose one.
|
||||
return undefined
|
||||
}
|
||||
;(fieldQuery as any)[targetK] = newV
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fieldQuery,
|
||||
disableUnset,
|
||||
disableEdit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function isCreateAllowed (_class: Ref<Class<Doc>>, space: Space): boolean {
|
||||
const c = getClient()
|
||||
const h = c.getHierarchy()
|
||||
|
||||
const rules = c.getModel().findAllSync(presentation.class.DocRules, { ofClass: _class })
|
||||
for (const r of rules) {
|
||||
if (r.createRule !== undefined) {
|
||||
if (r.createRule.mixin !== undefined) {
|
||||
if (h.hasMixin(space, r.createRule.mixin)) {
|
||||
const _mixin = h.as(space, r.createRule.mixin)
|
||||
if (matchQuery([_mixin], r.createRule.disallowQuery, r.createRule.mixin ?? space._class, h).length === 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (matchQuery([space], r.createRule.disallowQuery, r.createRule.mixin ?? space._class, h).length === 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
@ -1,4 +1,15 @@
|
||||
import { Client, Doc, RelatedDocument } from '@hcengineering/core'
|
||||
import {
|
||||
Class,
|
||||
Client,
|
||||
Doc,
|
||||
DocData,
|
||||
DocumentQuery,
|
||||
Mixin,
|
||||
Ref,
|
||||
RelatedDocument,
|
||||
Space,
|
||||
TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
import { AnyComponent, AnySvelteComponent, ComponentExtensionId } from '@hcengineering/ui'
|
||||
|
||||
@ -48,21 +59,93 @@ export interface ObjectSearchCategory extends Doc {
|
||||
query: Resource<ObjectSearchFactory>
|
||||
}
|
||||
|
||||
export interface ComponentExt {
|
||||
component: AnyComponent
|
||||
props?: Record<string, any>
|
||||
order?: number // Positioning of elements, into groups.
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* An component extension to various places of platform.
|
||||
*/
|
||||
export interface ComponentPointExtension extends Doc {
|
||||
export interface ComponentPointExtension extends Doc, ComponentExt {
|
||||
// Extension point we should extend.
|
||||
extension: ComponentExtensionId
|
||||
|
||||
// Component to be instantiated with at least following properties:
|
||||
// size: 'tiny' | 'small' | 'medium' | 'large'
|
||||
component: AnyComponent
|
||||
|
||||
// Extra properties to be passed to the component
|
||||
props?: Record<string, any>
|
||||
|
||||
order?: number // Positioning of elements, into groups.
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DocCreateFunction = (
|
||||
client: TxOperations,
|
||||
id: Ref<Doc>,
|
||||
space: Ref<Space>,
|
||||
document: DocData<Doc>,
|
||||
|
||||
extraData: Record<string, any>
|
||||
) => Promise<void>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type CreateExtensionKind = 'header' | 'title' | 'body' | 'footer' | 'pool' | 'buttons'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Customization for document creation
|
||||
*
|
||||
* Allow to customize create document/move issue dialogs, in case of selecting project of special kind.
|
||||
*/
|
||||
export interface DocCreateExtension extends Doc {
|
||||
ofClass: Ref<Class<Doc>>
|
||||
|
||||
components: Partial<Record<CreateExtensionKind, AnyComponent>>
|
||||
apply: Resource<DocCreateFunction>
|
||||
}
|
||||
|
||||
export interface DocAttributeRule {
|
||||
// A field name
|
||||
field: string
|
||||
|
||||
// If document is matched, rule will be used.
|
||||
query: DocumentQuery<Doc>
|
||||
|
||||
// If specified, will check for mixin to exists and cast to it
|
||||
mixin?: Ref<Mixin<Doc>>
|
||||
|
||||
// If specified, should be applied to field value queries, if field is reference to some document.
|
||||
fieldQuery?: DocumentQuery<Doc>
|
||||
// If specified will fill document properties to fieldQuery
|
||||
fieldQueryFill?: Record<string, string>
|
||||
|
||||
// If specified, should disable unset of field value.
|
||||
disableUnset?: boolean
|
||||
|
||||
// If specified should disable edit of this field value.
|
||||
disableEdit?: boolean
|
||||
|
||||
// In case of conflict values for multiple documents, will not be applied
|
||||
// Or will continue processing
|
||||
allowConflict?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A configurable rule's for some type of document
|
||||
*/
|
||||
export interface DocRules extends Doc {
|
||||
// Could be mixin, will be applied if mixin will be set for document.
|
||||
ofClass: Ref<Class<Doc>>
|
||||
|
||||
// attribute modification rules
|
||||
fieldRules: DocAttributeRule[]
|
||||
|
||||
// Check if document create is allowed for project based on query.
|
||||
createRule?: {
|
||||
// If query matched, document create is disallowed.
|
||||
disallowQuery: DocumentQuery<Space>
|
||||
mixin?: Ref<Mixin<Space>>
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ import view, { AttributeEditor } from '@hcengineering/view'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { KeyedAttribute } from '..'
|
||||
import { PresentationPipeline, PresentationPipelineImpl } from './pipeline'
|
||||
import { OptimizeQueryMiddleware, PresentationPipeline, PresentationPipelineImpl } from './pipeline'
|
||||
import plugin from './plugin'
|
||||
|
||||
let liveQuery: LQ
|
||||
@ -115,7 +115,7 @@ export async function setClient (_client: Client): Promise<void> {
|
||||
const factories = await _client.findAll(plugin.class.PresentationMiddlewareFactory, {})
|
||||
const promises = factories.map(async (it) => await getResource(it.createPresentationMiddleware))
|
||||
const creators = await Promise.all(promises)
|
||||
pipeline = PresentationPipelineImpl.create(_client, creators)
|
||||
pipeline = PresentationPipelineImpl.create(_client, [OptimizeQueryMiddleware.create, ...creators])
|
||||
|
||||
const needRefresh = liveQuery !== undefined
|
||||
liveQuery = new LQ(pipeline)
|
||||
|
@ -63,6 +63,7 @@
|
||||
on:delete
|
||||
on:action
|
||||
on:valid
|
||||
on:validate
|
||||
>
|
||||
<slot />
|
||||
</Ctor>
|
||||
@ -79,6 +80,7 @@
|
||||
on:delete
|
||||
on:action
|
||||
on:valid
|
||||
on:validate
|
||||
/>
|
||||
{/if}
|
||||
</ErrorBoundary>
|
||||
|
@ -16,9 +16,11 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import Spinner from './Spinner.svelte'
|
||||
import { ButtonSize } from '../types'
|
||||
|
||||
export let shrink: boolean = false
|
||||
export let label: string = ''
|
||||
export let size: ButtonSize = 'medium'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let timer: any
|
||||
@ -34,7 +36,7 @@
|
||||
|
||||
<div class="spinner-container" class:fullSize={!shrink}>
|
||||
<div data-label={label} class="inner" class:labeled={label !== ''}>
|
||||
<Spinner />
|
||||
<Spinner {size} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -13,44 +13,26 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { deviceOptionsStore, resizeObserver } from '..'
|
||||
import { createFocusManager } from '../focus'
|
||||
import type { AnySvelteComponent } from '../types'
|
||||
import type { SelectPopupValueType } from '../types'
|
||||
import EditWithIcon from './EditWithIcon.svelte'
|
||||
import FocusHandler from './FocusHandler.svelte'
|
||||
import Icon from './Icon.svelte'
|
||||
import IconCheck from './icons/Check.svelte'
|
||||
import IconSearch from './icons/Search.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import ListView from './ListView.svelte'
|
||||
|
||||
interface ValueType {
|
||||
id: number | string | null
|
||||
icon?: Asset | AnySvelteComponent
|
||||
iconProps?: Record<string, any>
|
||||
iconColor?: string
|
||||
label?: IntlString
|
||||
text?: string
|
||||
isSelected?: boolean
|
||||
|
||||
component?: AnySvelteComponent
|
||||
props?: Record<string, any>
|
||||
|
||||
category?: {
|
||||
icon?: Asset
|
||||
label: IntlString
|
||||
}
|
||||
}
|
||||
import IconCheck from './icons/Check.svelte'
|
||||
import IconSearch from './icons/Search.svelte'
|
||||
|
||||
export let placeholder: IntlString | undefined = undefined
|
||||
export let placeholderParam: any | undefined = undefined
|
||||
export let searchable: boolean = false
|
||||
export let value: Array<ValueType>
|
||||
export let value: Array<SelectPopupValueType>
|
||||
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||
export let onSelect: ((value: ValueType['id']) => void) | undefined = undefined
|
||||
export let onSelect: ((value: SelectPopupValueType['id']) => void) | undefined = undefined
|
||||
export let showShadow: boolean = true
|
||||
export let embedded: boolean = false
|
||||
|
||||
@ -63,7 +45,7 @@
|
||||
let selection = 0
|
||||
let list: ListView
|
||||
|
||||
function sendSelect (id: ValueType['id']): void {
|
||||
function sendSelect (id: SelectPopupValueType['id']): void {
|
||||
if (onSelect) {
|
||||
onSelect(id)
|
||||
} else {
|
||||
|
@ -447,3 +447,24 @@ export interface SeparatedElement {
|
||||
resize: boolean
|
||||
float?: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SelectPopupValueType {
|
||||
id: number | string | null
|
||||
icon?: Asset | AnySvelteComponent
|
||||
iconProps?: Record<string, any>
|
||||
iconColor?: string
|
||||
label?: IntlString
|
||||
text?: string
|
||||
isSelected?: boolean
|
||||
|
||||
component?: AnySvelteComponent
|
||||
props?: Record<string, any>
|
||||
|
||||
category?: {
|
||||
icon?: Asset
|
||||
label: IntlString
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
import AccountArrayEditor from './components/AccountArrayEditor.svelte'
|
||||
import AccountBox from './components/AccountBox.svelte'
|
||||
import AssigneeBox from './components/AssigneeBox.svelte'
|
||||
import AssigneePopup from './components/AssigneePopup.svelte'
|
||||
import Avatar from './components/Avatar.svelte'
|
||||
import ChannelFilter from './components/ChannelFilter.svelte'
|
||||
import ChannelPanel from './components/ChannelPanel.svelte'
|
||||
@ -134,6 +135,7 @@ export {
|
||||
EditableAvatar,
|
||||
UserBox,
|
||||
AssigneeBox,
|
||||
AssigneePopup,
|
||||
Avatar,
|
||||
UsersPopup,
|
||||
EmployeeBox,
|
||||
|
@ -22,6 +22,8 @@
|
||||
import preference, { SpacePreference } from '@hcengineering/preference'
|
||||
import {
|
||||
Card,
|
||||
DocCreateExtComponent,
|
||||
DocCreateExtensionManager,
|
||||
DraftController,
|
||||
KeyedAttribute,
|
||||
MessageBox,
|
||||
@ -40,7 +42,6 @@
|
||||
IssueTemplate,
|
||||
Milestone,
|
||||
Project,
|
||||
ProjectIssueTargetOptions,
|
||||
calcRank
|
||||
} from '@hcengineering/tracker'
|
||||
import {
|
||||
@ -61,7 +62,7 @@
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { activeComponent, activeMilestone, generateIssueShortLink, getIssueId, updateIssueRelation } from '../issues'
|
||||
import tracker from '../plugin'
|
||||
import ComponentSelector from './ComponentSelector.svelte'
|
||||
import ComponentSelector from './components/ComponentSelector.svelte'
|
||||
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
|
||||
import SubIssues from './SubIssues.svelte'
|
||||
import AssigneeEditor from './issues/AssigneeEditor.svelte'
|
||||
@ -300,17 +301,7 @@
|
||||
currentProject = res.shift()
|
||||
})
|
||||
|
||||
$: targetSettings =
|
||||
currentProject !== undefined
|
||||
? client
|
||||
.getHierarchy()
|
||||
.findClassOrMixinMixin<Class<Doc>, ProjectIssueTargetOptions>(
|
||||
currentProject,
|
||||
tracker.mixin.ProjectIssueTargetOptions
|
||||
)
|
||||
: undefined
|
||||
|
||||
let targetSettingOptions: Record<string, any> = {}
|
||||
const docCreateManager = DocCreateExtensionManager.create(tracker.class.Issue)
|
||||
|
||||
async function updateIssueStatusId (object: IssueDraft, currentProject: Project | undefined) {
|
||||
if (currentProject?.defaultIssueStatus && object.status === undefined) {
|
||||
@ -359,6 +350,8 @@
|
||||
return
|
||||
}
|
||||
|
||||
const operations = client.apply(_id)
|
||||
|
||||
const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
|
||||
const incResult = await client.updateDoc(
|
||||
tracker.class.Project,
|
||||
@ -395,12 +388,9 @@
|
||||
childInfo: []
|
||||
}
|
||||
|
||||
if (targetSettings !== undefined) {
|
||||
const updateOp = await getResource(targetSettings.update)
|
||||
updateOp?.(_id, _space as Ref<Project>, value, targetSettingOptions)
|
||||
}
|
||||
await docCreateManager.commit(operations, _id, _space, value)
|
||||
|
||||
await client.addCollection(
|
||||
await operations.addCollection(
|
||||
tracker.class.Issue,
|
||||
_space,
|
||||
parentIssue?._id ?? tracker.ids.NoParent,
|
||||
@ -410,7 +400,7 @@
|
||||
_id
|
||||
)
|
||||
for (const label of object.labels) {
|
||||
await client.addCollection(label._class, label.space, _id, tracker.class.Issue, 'labels', {
|
||||
await operations.addCollection(label._class, label.space, _id, tracker.class.Issue, 'labels', {
|
||||
title: label.title,
|
||||
color: label.color,
|
||||
tag: label.tag
|
||||
@ -418,11 +408,13 @@
|
||||
}
|
||||
await descriptionBox.createAttachments(_id)
|
||||
|
||||
await operations.commit()
|
||||
|
||||
if (relatedTo !== undefined) {
|
||||
const doc = await client.findOne(tracker.class.Issue, { _id })
|
||||
if (doc !== undefined) {
|
||||
if (client.getHierarchy().isDerived(relatedTo._class, tracker.class.Issue)) {
|
||||
await updateIssueRelation(client, relatedTo as Issue, doc, 'relations', '$push')
|
||||
await updateIssueRelation(operations, relatedTo as Issue, doc, 'relations', '$push')
|
||||
} else {
|
||||
const update = await getResource(chunter.backreference.Update)
|
||||
await update(doc, 'relations', [relatedTo], tracker.string.AddedReference)
|
||||
@ -585,6 +577,17 @@
|
||||
return client.findOne(tracker.class.Project, { _id: targetRef })
|
||||
}
|
||||
}
|
||||
|
||||
$: extraProps = {
|
||||
status: object.status,
|
||||
priority: object.priority,
|
||||
assignee: object.assignee,
|
||||
component: object.component,
|
||||
milestone: object.milestone,
|
||||
relatedTo,
|
||||
parentIssue,
|
||||
originalIssue
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -629,15 +632,7 @@
|
||||
docProps={{ disabled: true, noUnderline: true }}
|
||||
focusIndex={20000}
|
||||
/>
|
||||
{#if targetSettings?.headerComponent && currentProject}
|
||||
<Component
|
||||
is={targetSettings.headerComponent}
|
||||
props={{ targetSettingOptions, project: currentProject }}
|
||||
on:change={(evt) => {
|
||||
targetSettingOptions = evt.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<DocCreateExtComponent manager={docCreateManager} kind={'header'} space={currentProject} props={extraProps} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title" let:label>
|
||||
<div class="flex-row-center gap-1">
|
||||
@ -660,6 +655,7 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<DocCreateExtComponent manager={docCreateManager} kind={'title'} space={currentProject} props={extraProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="subheader">
|
||||
@ -720,15 +716,7 @@
|
||||
bind:subIssues={object.subIssues}
|
||||
/>
|
||||
{/if}
|
||||
{#if targetSettings?.bodyComponent && currentProject}
|
||||
<Component
|
||||
is={targetSettings.bodyComponent}
|
||||
props={{ targetSettingOptions, project: currentProject }}
|
||||
on:change={(evt) => {
|
||||
targetSettingOptions = evt.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<DocCreateExtComponent manager={docCreateManager} kind={'body'} space={currentProject} props={extraProps} />
|
||||
<svelte:fragment slot="pool">
|
||||
<div id="status-editor">
|
||||
<StatusEditor
|
||||
@ -804,7 +792,6 @@
|
||||
isEditable={true}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
short
|
||||
/>
|
||||
<div id="estimation-editor" class="new-line">
|
||||
<EstimationEditor focusIndex={7} kind={'regular'} size={'large'} value={object} />
|
||||
@ -839,15 +826,7 @@
|
||||
on:click={object.parentIssue ? clearParentIssue : setParentIssue}
|
||||
/>
|
||||
</div>
|
||||
{#if targetSettings?.poolComponent && currentProject}
|
||||
<Component
|
||||
is={targetSettings.poolComponent}
|
||||
props={{ targetSettingOptions, project: currentProject }}
|
||||
on:change={(evt) => {
|
||||
targetSettingOptions = evt.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<DocCreateExtComponent manager={docCreateManager} kind={'pool'} space={currentProject} props={extraProps} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="attachments">
|
||||
{#if attachments.size > 0}
|
||||
@ -874,14 +853,9 @@
|
||||
descriptionBox.handleAttach()
|
||||
}}
|
||||
/>
|
||||
{#if targetSettings?.footerComponent && currentProject}
|
||||
<Component
|
||||
is={targetSettings.footerComponent}
|
||||
props={{ targetSettingOptions, project: currentProject }}
|
||||
on:change={(evt) => {
|
||||
targetSettingOptions = evt.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<DocCreateExtComponent manager={docCreateManager} kind={'footer'} space={currentProject} props={extraProps} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<DocCreateExtComponent manager={docCreateManager} kind={'buttons'} space={currentProject} props={extraProps} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
@ -27,9 +27,10 @@
|
||||
} from '@hcengineering/view-resources'
|
||||
import { onDestroy } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { ComponentsFilterMode, componentsTitleMap } from '../../utils'
|
||||
import { ComponentsFilterMode, activeProjects, componentsTitleMap } from '../../utils'
|
||||
import ComponentsContent from './ComponentsContent.svelte'
|
||||
import NewComponent from './NewComponent.svelte'
|
||||
import { isCreateAllowed } from '@hcengineering/presentation'
|
||||
|
||||
export let label: IntlString
|
||||
export let query: DocumentQuery<Component> = {}
|
||||
@ -39,6 +40,8 @@
|
||||
|
||||
const space = typeof query.space === 'string' ? query.space : tracker.project.DefaultProject
|
||||
|
||||
$: project = $activeProjects.get(space)
|
||||
|
||||
let viewlet: WithLookup<Viewlet> | undefined
|
||||
let viewlets: WithLookup<Viewlet>[] | undefined
|
||||
let viewletKey = makeViewletKey()
|
||||
@ -87,7 +90,9 @@
|
||||
|
||||
<div class="ac-header-full medium-gap mb-1">
|
||||
<ViewletSelector bind:viewlet bind:viewlets viewletQuery={{ attachTo: tracker.class.Component }} />
|
||||
<Button icon={IconAdd} label={tracker.string.Component} kind="primary" on:click={showCreateDialog} />
|
||||
{#if project !== undefined && isCreateAllowed(tracker.class.Component, project)}
|
||||
<Button icon={IconAdd} label={tracker.string.Component} kind="primary" on:click={showCreateDialog} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ac-header full divide search-start">
|
||||
|
@ -13,29 +13,30 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachedData, Ref } from '@hcengineering/core'
|
||||
import { AttachedData, DocumentQuery, Ref } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { RuleApplyResult, createQuery, getClient, getDocRules } from '@hcengineering/presentation'
|
||||
import { Component, Issue, IssueTemplate, Project } from '@hcengineering/tracker'
|
||||
import { ButtonKind, ButtonShape, ButtonSize, tooltip } from '@hcengineering/ui'
|
||||
import { ButtonKind, ButtonShape, ButtonSize, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { activeComponent } from '../../issues'
|
||||
import tracker from '../../plugin'
|
||||
import ComponentSelector from '../ComponentSelector.svelte'
|
||||
import ComponentSelector from './ComponentSelector.svelte'
|
||||
|
||||
export let value: Issue | IssueTemplate | AttachedData<Issue>
|
||||
export let value: Issue | Issue[] | IssueTemplate | AttachedData<Issue>
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let popupPlaceholder: IntlString = tracker.string.MoveToComponent
|
||||
export let shouldShowPlaceholder = true
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let shape: ButtonShape = undefined
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
export let width: string | undefined = '100%'
|
||||
export let onlyIcon: boolean = false
|
||||
export let isAction: boolean = false
|
||||
export let groupBy: string | undefined = undefined
|
||||
export let enlargedText = false
|
||||
export let enlargedText: boolean = false
|
||||
export let compression: boolean = false
|
||||
export let shrink: number = 0
|
||||
export let space: Ref<Project> | undefined = undefined
|
||||
@ -45,46 +46,129 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const handleComponentIdChanged = async (newComponentId: Ref<Component> | null | undefined) => {
|
||||
if (!isEditable || newComponentId === undefined || value.component === newComponentId) {
|
||||
if (!isEditable || newComponentId === undefined || (!Array.isArray(value) && value.component === newComponentId)) {
|
||||
return
|
||||
}
|
||||
dispatch('change', newComponentId)
|
||||
if ('_class' in value) {
|
||||
await client.update(value, { component: newComponentId })
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
await Promise.all(
|
||||
value.map(async (p) => {
|
||||
if ('_class' in value) {
|
||||
await client.update(p, { component: newComponentId })
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
if ('_class' in value) {
|
||||
await client.update(value, { component: newComponentId })
|
||||
}
|
||||
}
|
||||
dispatch('change', newComponentId)
|
||||
if (isAction) dispatch('close')
|
||||
}
|
||||
|
||||
$: _space = space ?? ('space' in value ? value.space : undefined)
|
||||
const milestoneQuery = createQuery()
|
||||
let component: Component | undefined
|
||||
$: if (!Array.isArray(value) && value.component) {
|
||||
milestoneQuery.query(tracker.class.Component, { _id: value.component }, (res) => {
|
||||
component = res.shift()
|
||||
})
|
||||
}
|
||||
|
||||
$: _space =
|
||||
space ??
|
||||
(Array.isArray(value)
|
||||
? { $in: Array.from(new Set(value.map((it) => it.space))) }
|
||||
: 'space' in value
|
||||
? value.space
|
||||
: undefined)
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
|
||||
let rulesQuery: RuleApplyResult<Component> | undefined
|
||||
let query: DocumentQuery<Component>
|
||||
$: if (Array.isArray(value) || '_id' in value) {
|
||||
rulesQuery = getDocRules<Component>(value, 'component')
|
||||
if (rulesQuery !== undefined) {
|
||||
query = { ...(rulesQuery?.fieldQuery ?? {}) }
|
||||
} else {
|
||||
query = { _id: 'none' as Ref<Component> }
|
||||
rulesQuery = {
|
||||
disableEdit: true,
|
||||
disableUnset: true,
|
||||
fieldQuery: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if (value.component && value.component !== $activeComponent && groupBy !== 'component') || shouldShowPlaceholder}
|
||||
{#if kind === 'list'}
|
||||
{#if !Array.isArray(value) && value.component}
|
||||
<div class={compression ? 'label-wrapper' : 'clear-mins'}>
|
||||
<ComponentSelector
|
||||
{kind}
|
||||
{size}
|
||||
{shape}
|
||||
{justify}
|
||||
isEditable={isEditable && !rulesQuery?.disableEdit}
|
||||
isAllowUnset={!rulesQuery?.disableUnset}
|
||||
{shouldShowLabel}
|
||||
{popupPlaceholder}
|
||||
{onlyIcon}
|
||||
{query}
|
||||
space={_space}
|
||||
{enlargedText}
|
||||
short={compression}
|
||||
showTooltip={{ label: value.component ? tracker.string.MoveToComponent : tracker.string.AddToComponent }}
|
||||
value={value.component}
|
||||
onChange={handleComponentIdChanged}
|
||||
{isAction}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class={compression ? 'label-wrapper' : 'clear-mins'}
|
||||
class="flex flex-wrap clear-mins"
|
||||
class:minus-margin={kind === 'list-header'}
|
||||
use:tooltip={{ label: value.component ? tracker.string.MoveToComponent : tracker.string.AddToComponent }}
|
||||
class:label-wrapper={compression}
|
||||
style:flex-direction={twoRows ? 'column' : 'row'}
|
||||
>
|
||||
<ComponentSelector
|
||||
{kind}
|
||||
{size}
|
||||
{shape}
|
||||
{width}
|
||||
{justify}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
{popupPlaceholder}
|
||||
{onlyIcon}
|
||||
{enlargedText}
|
||||
{shrink}
|
||||
space={_space}
|
||||
value={value.component}
|
||||
short={compression}
|
||||
onChange={handleComponentIdChanged}
|
||||
/>
|
||||
{#if (!Array.isArray(value) && value.component && value.component !== $activeComponent && groupBy !== 'component') || shouldShowPlaceholder}
|
||||
<div class="flex-row-center" class:minus-margin-vSpace={kind === 'list-header'} class:compression style:width>
|
||||
<ComponentSelector
|
||||
{kind}
|
||||
{size}
|
||||
{shape}
|
||||
{width}
|
||||
{justify}
|
||||
isEditable={isEditable && !rulesQuery?.disableEdit}
|
||||
isAllowUnset={!rulesQuery?.disableUnset}
|
||||
{shouldShowLabel}
|
||||
{popupPlaceholder}
|
||||
{onlyIcon}
|
||||
{enlargedText}
|
||||
{query}
|
||||
space={_space}
|
||||
showTooltip={{
|
||||
label:
|
||||
!Array.isArray(value) && value.component ? tracker.string.MoveToComponent : tracker.string.AddToComponent
|
||||
}}
|
||||
value={!Array.isArray(value) ? value.component : undefined}
|
||||
onChange={handleComponentIdChanged}
|
||||
{isAction}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.minus-margin {
|
||||
margin-left: -0.5rem;
|
||||
&-vSpace {
|
||||
margin: -0.25rem 0;
|
||||
}
|
||||
&-space {
|
||||
margin: -0.25rem 0 -0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -14,12 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Component } from '@hcengineering/tracker'
|
||||
import { Icon, tooltip, themeStore } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { Icon, Component as UIComponent, themeStore, tooltip } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { DocNavLink } from '@hcengineering/view-resources'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: WithLookup<Component> | undefined
|
||||
export let shouldShowAvatar = true
|
||||
@ -44,21 +45,46 @@
|
||||
})
|
||||
}
|
||||
$: disabled = disabled || value === undefined
|
||||
|
||||
$: presenters =
|
||||
value !== undefined ? getClient().getHierarchy().findMixinMixins(value, view.mixin.ObjectPresenter) : []
|
||||
|
||||
$: icon = tracker.icon.Component
|
||||
</script>
|
||||
|
||||
<DocNavLink object={value} {onClick} {disabled} {noUnderline} {inline} {accent} component={view.component.EditDoc}>
|
||||
{#if inline}
|
||||
<span class="antiMention" use:tooltip={{ label: tracker.string.Component }}>@{label}</span>
|
||||
{:else}
|
||||
<span class="flex-presenter" class:list={kind === 'list'} use:tooltip={{ label: tracker.string.Component }}>
|
||||
{#if shouldShowAvatar}
|
||||
<div class="icon">
|
||||
<Icon icon={tracker.icon.Component} size={'small'} />
|
||||
<div class="flex-row-center">
|
||||
<DocNavLink object={value} {onClick} {disabled} {noUnderline} {inline} {accent} component={view.component.EditDoc}>
|
||||
{#if inline}
|
||||
<span class="antiMention" use:tooltip={{ label: tracker.string.Component }}>@{label}</span>
|
||||
{:else}
|
||||
<span class="flex-presenter flex-row-center" class:list={kind === 'list'}>
|
||||
<div class="flex-row-center">
|
||||
{#if shouldShowAvatar}
|
||||
<div class="icon">
|
||||
<Icon icon={presenters.length === 0 ? tracker.icon.Component : icon} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<span title={label} class="label nowrap" class:no-underline={disabled || noUnderline} class:fs-bold={accent}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<span title={label} class="label nowrap" class:no-underline={disabled || noUnderline} class:fs-bold={accent}>
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</DocNavLink>
|
||||
|
||||
{#if presenters.length > 0}
|
||||
<div class="flex-row-center">
|
||||
{#each presenters as mixinPresenter}
|
||||
<UIComponent
|
||||
is={mixinPresenter.presenter}
|
||||
props={{ value }}
|
||||
on:open={(evt) => {
|
||||
if (evt.detail.icon !== undefined) {
|
||||
icon = evt.detail.icon
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</DocNavLink>
|
||||
</div>
|
||||
|
@ -13,17 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { IntlString, getEmbeddedLabel, translate } from '@hcengineering/platform'
|
||||
import { DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Component, Project } from '@hcengineering/tracker'
|
||||
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import { Button, ButtonShape, Label, SelectPopup, eventToHTMLElement, showPopup, themeStore } from '@hcengineering/ui'
|
||||
import tracker from '../plugin'
|
||||
import { Component } from '@hcengineering/tracker'
|
||||
import type { ButtonKind, ButtonSize, LabelAndProps, SelectPopupValueType } from '@hcengineering/ui'
|
||||
import { Button, ButtonShape, SelectPopup, eventToHTMLElement, showPopup, themeStore } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import ComponentPresenter from './ComponentPresenter.svelte'
|
||||
|
||||
export let value: Ref<Component> | null | undefined
|
||||
export let space: DocumentQuery<Component>['space'] | undefined = undefined
|
||||
export let query: DocumentQuery<Component> = {}
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let isEditable: boolean = false
|
||||
export let isEditable: boolean = true
|
||||
export let onChange: ((newComponentId: Ref<Component> | undefined) => void) | undefined = undefined
|
||||
export let popupPlaceholder: IntlString = tracker.string.AddToComponent
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
@ -34,22 +37,21 @@
|
||||
export let onlyIcon: boolean = false
|
||||
export let enlargedText: boolean = false
|
||||
export let short: boolean = false
|
||||
export let shrink: number = 0
|
||||
export let focusIndex: number | undefined = undefined
|
||||
export let space: Ref<Project> | undefined = undefined
|
||||
export let isAction: boolean = false
|
||||
export let isAllowUnset = true
|
||||
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
let selectedComponent: Component | undefined
|
||||
let defaultComponentLabel = ''
|
||||
|
||||
const query = createQuery()
|
||||
const queryQuery = createQuery()
|
||||
let rawComponents: Component[] = []
|
||||
let loading = true
|
||||
$: query.query(
|
||||
$: queryQuery.query(
|
||||
tracker.class.Component,
|
||||
space !== undefined ? { space } : {},
|
||||
{ ...query, ...(space ? { space } : {}) },
|
||||
(res) => {
|
||||
rawComponents = res
|
||||
loading = false
|
||||
},
|
||||
{
|
||||
sort: { modifiedOn: SortingOrder.Ascending }
|
||||
@ -59,7 +61,6 @@
|
||||
$: handleSelectedComponentIdUpdated(value, rawComponents)
|
||||
|
||||
$: translate(tracker.string.NoComponent, {}, $themeStore.language).then((result) => (defaultComponentLabel = result))
|
||||
$: componentText = shouldShowLabel ? selectedComponent?.label ?? defaultComponentLabel : undefined
|
||||
|
||||
const handleSelectedComponentIdUpdated = async (
|
||||
newComponentId: Ref<Component> | null | undefined,
|
||||
@ -74,45 +75,57 @@
|
||||
selectedComponent = components.find((it) => it._id === newComponentId)
|
||||
}
|
||||
|
||||
function getComponentInfo (rawComponents: Component[], sp: Component | undefined): SelectPopupValueType[] {
|
||||
return [
|
||||
...(isAllowUnset
|
||||
? [
|
||||
{
|
||||
id: null,
|
||||
icon: tracker.icon.Component,
|
||||
label: tracker.string.NoComponent,
|
||||
isSelected: sp === undefined
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...rawComponents.map((p) => ({
|
||||
id: p._id,
|
||||
icon: tracker.icon.Component,
|
||||
text: p.label,
|
||||
isSelected: sp ? p._id === sp._id : false,
|
||||
component: ComponentPresenter,
|
||||
props: {
|
||||
value: p
|
||||
}
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
let components: SelectPopupValueType[] = []
|
||||
$: components = getComponentInfo(rawComponents, selectedComponent)
|
||||
|
||||
const handleComponentEditorOpened = async (event: MouseEvent): Promise<void> => {
|
||||
event.stopPropagation()
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
|
||||
const componentsInfo = [
|
||||
{ id: null, icon: tracker.icon.Components, label: tracker.string.NoComponent, isSelected: !selectedComponent },
|
||||
...rawComponents.map((p) => ({
|
||||
id: p._id,
|
||||
icon: tracker.icon.Components,
|
||||
text: p.label,
|
||||
isSelected: selectedComponent ? p._id === selectedComponent._id : false
|
||||
}))
|
||||
]
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{ value: componentsInfo, placeholder: popupPlaceholder, searchable: true },
|
||||
{ value: components, placeholder: popupPlaceholder, searchable: true },
|
||||
eventToHTMLElement(event),
|
||||
onChange
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if onlyIcon || componentText === undefined}
|
||||
<Button
|
||||
{focusIndex}
|
||||
{kind}
|
||||
{size}
|
||||
{shape}
|
||||
{width}
|
||||
{justify}
|
||||
icon={tracker.icon.Components}
|
||||
disabled={!isEditable}
|
||||
{loading}
|
||||
{short}
|
||||
{shrink}
|
||||
on:click={handleComponentEditorOpened}
|
||||
{#if isAction}
|
||||
<SelectPopup
|
||||
value={components}
|
||||
placeholder={popupPlaceholder}
|
||||
searchable
|
||||
on:close={(evt) => {
|
||||
if (onChange !== undefined) onChange(evt.detail)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
@ -122,17 +135,15 @@
|
||||
{shape}
|
||||
{width}
|
||||
{justify}
|
||||
icon={tracker.icon.Components}
|
||||
{showTooltip}
|
||||
disabled={!isEditable}
|
||||
{loading}
|
||||
notSelected={!value}
|
||||
{short}
|
||||
{shrink}
|
||||
on:click={handleComponentEditorOpened}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<span class="label {enlargedText ? 'ml-1 text-base' : 'text-md'} overflow-label pointer-events-none">
|
||||
<Label label={getEmbeddedLabel(componentText)} />
|
||||
<span class="label {enlargedText ? 'text-base' : 'text-md'} overflow-label pointer-events-none">
|
||||
<svelte:component this={ComponentPresenter} value={selectedComponent} />
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
</Button>
|
@ -13,15 +13,15 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { EmployeeBox } from '@hcengineering/contact-resources'
|
||||
import { Data, Ref } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Card, getClient, SpaceSelector } from '@hcengineering/presentation'
|
||||
import { EmployeeBox } from '@hcengineering/contact-resources'
|
||||
import { Card, SpaceSelector, getClient } from '@hcengineering/presentation'
|
||||
import { StyledTextArea } from '@hcengineering/text-editor'
|
||||
import { Component, Project } from '@hcengineering/tracker'
|
||||
import { EditBox } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { StyledTextArea } from '@hcengineering/text-editor'
|
||||
import ProjectPresenter from '../projects/ProjectPresenter.svelte'
|
||||
|
||||
export let space: Ref<Project>
|
||||
@ -36,8 +36,10 @@
|
||||
attachments: 0
|
||||
}
|
||||
|
||||
let _space = space
|
||||
|
||||
async function onSave () {
|
||||
await client.createDoc(tracker.class.Component, space, object)
|
||||
await client.createDoc(tracker.class.Component, _space, object)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -54,7 +56,7 @@
|
||||
<SpaceSelector
|
||||
_class={tracker.class.Project}
|
||||
label={tracker.string.Project}
|
||||
bind:space
|
||||
bind:space={_space}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
component={ProjectPresenter}
|
||||
|
@ -13,21 +13,22 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Employee, Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { AssigneeBox, personAccountByIdStore } from '@hcengineering/contact-resources'
|
||||
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { AssigneeBox, AssigneePopup, personAccountByIdStore } from '@hcengineering/contact-resources'
|
||||
import { AssigneeCategory } from '@hcengineering/contact-resources/src/assignee'
|
||||
import { Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
||||
import { Account, Doc, DocumentQuery, Ref, Space } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { Component, Issue } from '@hcengineering/tracker'
|
||||
import { ButtonKind, ButtonSize, IconSize, TooltipAlignment } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import tracker from '../../plugin'
|
||||
import { getPreviousAssignees } from '../../utils'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
type Object = (Doc | {}) & Pick<Issue, 'space' | 'component' | 'assignee'>
|
||||
|
||||
export let object: Object
|
||||
export let object: Object | Object[] | undefined = undefined
|
||||
export let value: Object | Object[] | undefined = undefined
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let avatarSize: IconSize = 'card'
|
||||
@ -37,57 +38,83 @@
|
||||
export let short: boolean = false
|
||||
export let shouldShowName = true
|
||||
export let shrink: number = 0
|
||||
export let isAction: boolean = false
|
||||
|
||||
$: _object = object ?? value ?? []
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const docQuery: DocumentQuery<Employee> = { active: true }
|
||||
|
||||
const handleAssigneeChanged = async (newAssignee: Ref<Person> | undefined) => {
|
||||
if (newAssignee === undefined || object.assignee === newAssignee) {
|
||||
const handleAssigneeChanged = async (newAssignee: Ref<Person> | undefined | null) => {
|
||||
if (newAssignee === undefined || (!Array.isArray(_object) && _object.assignee === newAssignee)) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('change', newAssignee)
|
||||
|
||||
if ('_class' in object) {
|
||||
await client.update(object, { assignee: newAssignee })
|
||||
if (Array.isArray(_object)) {
|
||||
await Promise.all(
|
||||
_object.map(async (p) => {
|
||||
if ('_class' in p) {
|
||||
await client.update(p, { assignee: newAssignee })
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
if ('_class' in _object) {
|
||||
await client.update(_object as any, { assignee: newAssignee })
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('change', newAssignee)
|
||||
if (isAction) dispatch('close')
|
||||
}
|
||||
|
||||
let categories: AssigneeCategory[] = []
|
||||
|
||||
function getCategories (object: Object): void {
|
||||
function getCategories (object: Object | Object[]): void {
|
||||
categories = []
|
||||
if ('_class' in object) {
|
||||
const _id = object._id
|
||||
const docs = Array.isArray(object) ? object : [object]
|
||||
const cdocs = docs.filter((d) => '_class' in d) as Doc[]
|
||||
if (cdocs.length > 0) {
|
||||
categories.push({
|
||||
label: tracker.string.PreviousAssigned,
|
||||
func: async () => await getPreviousAssignees(_id)
|
||||
func: async () => {
|
||||
const r: Ref<Person>[] = []
|
||||
for (const d of cdocs) {
|
||||
r.push(...(await getPreviousAssignees(d._id)))
|
||||
}
|
||||
return r
|
||||
}
|
||||
})
|
||||
}
|
||||
categories.push({
|
||||
label: tracker.string.ComponentLead,
|
||||
func: async () => {
|
||||
if (!object.component) {
|
||||
const components = Array.from(docs.map((it) => it.component).filter((it) => it)) as Ref<Component>[]
|
||||
if (components.length === 0) {
|
||||
return []
|
||||
}
|
||||
const component = await client.findOne(tracker.class.Component, { _id: object.component })
|
||||
return component?.lead ? [component.lead] : []
|
||||
const component = await client.findAll(tracker.class.Component, { _id: { $in: components } })
|
||||
return component.map((it) => it.lead).filter((it) => it) as Ref<Person>[]
|
||||
}
|
||||
})
|
||||
categories.push({
|
||||
label: tracker.string.Members,
|
||||
func: async () => {
|
||||
if (!object.space) {
|
||||
const spaces = Array.from(docs.map((it) => it.space).filter((it) => it)) as Ref<Space>[]
|
||||
if (spaces.length === 0) {
|
||||
return []
|
||||
}
|
||||
const project = await client.findOne(tracker.class.Project, { _id: object.space })
|
||||
if (project === undefined) {
|
||||
const projects = await client.findAll(tracker.class.Project, {
|
||||
_id: !Array.isArray(object) ? object.space : { $in: Array.from(object.map((it) => it.space)) }
|
||||
})
|
||||
if (projects === undefined) {
|
||||
return []
|
||||
}
|
||||
const store = get(personAccountByIdStore)
|
||||
const accounts = project.members
|
||||
const allMembers = projects.reduce((arr, p) => arr.concat(p.members), [] as Ref<Account>[])
|
||||
const accounts = allMembers
|
||||
.map((p) => store.get(p as Ref<PersonAccount>))
|
||||
.filter((p) => p !== undefined) as PersonAccount[]
|
||||
return accounts.map((p) => p.person as Ref<Employee>)
|
||||
@ -95,33 +122,59 @@
|
||||
})
|
||||
}
|
||||
|
||||
$: getCategories(object)
|
||||
$: getCategories(_object)
|
||||
|
||||
$: sel =
|
||||
(!Array.isArray(_object)
|
||||
? _object.assignee
|
||||
: _object.reduce((v, it) => (v != null && v === it.assignee ? it.assignee : null), _object[0]?.assignee) ??
|
||||
undefined) ?? undefined
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
<AssigneeBox
|
||||
{docQuery}
|
||||
{focusIndex}
|
||||
label={tracker.string.Assignee}
|
||||
placeholder={tracker.string.Assignee}
|
||||
value={object.assignee}
|
||||
{categories}
|
||||
titleDeselect={tracker.string.Unassigned}
|
||||
{size}
|
||||
{kind}
|
||||
{avatarSize}
|
||||
{width}
|
||||
{short}
|
||||
{shrink}
|
||||
{shouldShowName}
|
||||
showNavigate={false}
|
||||
justify={'left'}
|
||||
showTooltip={{
|
||||
label: tracker.string.AssignTo,
|
||||
personLabel: tracker.string.AssignedTo,
|
||||
placeholderLabel: tracker.string.Unassigned,
|
||||
direction: tooltipAlignment
|
||||
}}
|
||||
on:change={({ detail }) => handleAssigneeChanged(detail)}
|
||||
/>
|
||||
{#if _object}
|
||||
{#if isAction}
|
||||
<AssigneePopup
|
||||
{docQuery}
|
||||
{categories}
|
||||
icon={contact.icon.Person}
|
||||
selected={sel}
|
||||
allowDeselect={true}
|
||||
titleDeselect={undefined}
|
||||
on:close={(evt) => {
|
||||
const result = evt.detail
|
||||
if (result === null) {
|
||||
handleAssigneeChanged(null)
|
||||
} else if (result !== undefined && result._id !== value) {
|
||||
value = result._id
|
||||
handleAssigneeChanged(result._id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<AssigneeBox
|
||||
{docQuery}
|
||||
{focusIndex}
|
||||
label={tracker.string.Assignee}
|
||||
placeholder={tracker.string.Assignee}
|
||||
value={sel}
|
||||
{categories}
|
||||
titleDeselect={tracker.string.Unassigned}
|
||||
{size}
|
||||
{kind}
|
||||
{avatarSize}
|
||||
{width}
|
||||
{short}
|
||||
{shrink}
|
||||
{shouldShowName}
|
||||
showNavigate={false}
|
||||
justify={'left'}
|
||||
showTooltip={{
|
||||
label: tracker.string.AssignTo,
|
||||
personLabel: tracker.string.AssignedTo,
|
||||
placeholderLabel: tracker.string.Unassigned,
|
||||
direction: tooltipAlignment
|
||||
}}
|
||||
on:change={({ detail }) => handleAssigneeChanged(detail)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -15,8 +15,10 @@
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import type { Issue, Project } from '@hcengineering/tracker'
|
||||
import { AnySvelteComponent, Icon, tooltip } from '@hcengineering/ui'
|
||||
import { AnySvelteComponent, Component, Icon, tooltip } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { DocNavLink } from '@hcengineering/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import { activeProjects } from '../../utils'
|
||||
@ -37,34 +39,47 @@
|
||||
}
|
||||
|
||||
$: title = currentProject ? `${currentProject.identifier}-${value?.number}` : `${value?.number}`
|
||||
|
||||
$: presenters =
|
||||
value !== undefined ? getClient().getHierarchy().findMixinMixins(value, view.mixin.ObjectPresenter) : []
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<DocNavLink
|
||||
object={value}
|
||||
{onClick}
|
||||
{disabled}
|
||||
{noUnderline}
|
||||
{inline}
|
||||
component={tracker.component.EditIssue}
|
||||
shrink={0}
|
||||
>
|
||||
{#if inline}
|
||||
<span class="antiMention" use:tooltip={{ label: tracker.string.Issue }}>@{title}</span>
|
||||
{:else}
|
||||
<span class="issuePresenterRoot" class:list={kind === 'list'} class:cursor-pointer={!disabled}>
|
||||
{#if shouldShowAvatar}
|
||||
<div class="icon" use:tooltip={{ label: tracker.string.Issue }}>
|
||||
<Icon icon={icon ?? tracker.icon.Issues} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<span class="overflow-label select-text" title={value?.title}>
|
||||
{title}
|
||||
<slot name="details" />
|
||||
<div class="flex-row-center flex-between">
|
||||
<DocNavLink
|
||||
object={value}
|
||||
{onClick}
|
||||
{disabled}
|
||||
{noUnderline}
|
||||
{inline}
|
||||
component={tracker.component.EditIssue}
|
||||
shrink={0}
|
||||
>
|
||||
{#if inline}
|
||||
<span class="antiMention" use:tooltip={{ label: tracker.string.Issue }}>@{title}</span>
|
||||
{:else}
|
||||
<span class="issuePresenterRoot" class:list={kind === 'list'} class:cursor-pointer={!disabled}>
|
||||
{#if shouldShowAvatar}
|
||||
<div class="icon" use:tooltip={{ label: tracker.string.Issue }}>
|
||||
<Icon icon={icon ?? tracker.icon.Issues} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<span class="overflow-label select-text" title={value?.title}>
|
||||
{title}
|
||||
<slot name="details" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</DocNavLink>
|
||||
{#if presenters.length > 0}
|
||||
<div class="flex-row-center">
|
||||
{#each presenters as mixinPresenter}
|
||||
{mixinPresenter.presenter}
|
||||
<Component is={mixinPresenter.presenter} props={{ value }} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</DocNavLink>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -102,11 +102,9 @@
|
||||
return
|
||||
}
|
||||
|
||||
const milestoneInfo = milestones
|
||||
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{ value: milestoneInfo, placeholder: popupPlaceholder, searchable: true },
|
||||
{ value: milestones, placeholder: popupPlaceholder, searchable: true },
|
||||
eventToHTMLElement(event),
|
||||
onChange
|
||||
)
|
||||
|
@ -23,7 +23,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { activeComponent, activeMilestone } from '../../issues'
|
||||
import tracker from '../../plugin'
|
||||
import ComponentSelector from '../ComponentSelector.svelte'
|
||||
import ComponentSelector from '../components/ComponentSelector.svelte'
|
||||
import AssigneeEditor from '../issues/AssigneeEditor.svelte'
|
||||
import PriorityEditor from '../issues/PriorityEditor.svelte'
|
||||
import MilestoneSelector from '../milestones/MilestoneSelector.svelte'
|
||||
|
@ -104,7 +104,7 @@ import TimeSpendReport from './components/issues/timereport/TimeSpendReport.svel
|
||||
import RelatedIssues from './components/issues/related/RelatedIssues.svelte'
|
||||
import RelatedIssueTemplates from './components/issues/related/RelatedIssueTemplates.svelte'
|
||||
|
||||
import ComponentSelector from './components/ComponentSelector.svelte'
|
||||
import ComponentSelector from './components/components/ComponentSelector.svelte'
|
||||
|
||||
import IssueTemplatePresenter from './components/templates/IssueTemplatePresenter.svelte'
|
||||
import IssueTemplates from './components/templates/IssueTemplates.svelte'
|
||||
|
@ -19,11 +19,9 @@ import {
|
||||
Attribute,
|
||||
Class,
|
||||
Doc,
|
||||
DocData,
|
||||
DocManager,
|
||||
IdMap,
|
||||
Markup,
|
||||
Mixin,
|
||||
Ref,
|
||||
RelatedDocument,
|
||||
Space,
|
||||
@ -78,33 +76,6 @@ export interface RelatedIssueTarget extends Doc {
|
||||
rule: RelatedClassRule | RelatedSpaceRule
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type IssueUpdateFunction = (
|
||||
id: Ref<Issue>,
|
||||
space: Ref<Space>,
|
||||
issue: DocData<Issue>,
|
||||
data: Record<string, any>
|
||||
) => Promise<void>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Customization mixin for project class.
|
||||
*
|
||||
* Allow to customize create issue/move issue dialogs, in case of selecting project of special kind.
|
||||
*/
|
||||
export interface ProjectIssueTargetOptions extends Class<Doc> {
|
||||
// Component receiving project and context data.
|
||||
headerComponent?: AnyComponent
|
||||
bodyComponent?: AnyComponent
|
||||
footerComponent?: AnyComponent
|
||||
poolComponent?: AnyComponent
|
||||
|
||||
update: Resource<IssueUpdateFunction>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -520,9 +491,6 @@ export default plugin(trackerId, {
|
||||
IssueAssigneedToYou: '' as IntlString,
|
||||
RelatedIssues: '' as IntlString
|
||||
},
|
||||
mixin: {
|
||||
ProjectIssueTargetOptions: '' as Ref<Mixin<ProjectIssueTargetOptions>>
|
||||
},
|
||||
extensions: {
|
||||
IssueListHeader: '' as ComponentExtensionId,
|
||||
EditIssueHeader: '' as ComponentExtensionId
|
||||
|
@ -416,14 +416,15 @@ function AttributeSelector (
|
||||
values?: Array<{ icon?: Asset, label: IntlString, id: number | string }>
|
||||
|
||||
isAction?: boolean
|
||||
|
||||
valueKey?: string
|
||||
}
|
||||
): void {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const docArray = Array.isArray(doc) ? doc : [doc]
|
||||
const attribute = hierarchy.getAttribute(docArray[0]._class, props.attribute)
|
||||
showPopup(props.actionPopup, { ...props, value: docArray, width: 'large' }, 'top', (result) => {
|
||||
console.log(result)
|
||||
showPopup(props.actionPopup, { ...props, [props.valueKey ?? 'value']: docArray, width: 'large' }, 'top', (result) => {
|
||||
if (result != null) {
|
||||
for (const docEl of docArray) {
|
||||
void updateAttribute(client, docEl, docEl._class, { key: props.attribute, attr: attribute }, result)
|
||||
|
@ -16,16 +16,17 @@
|
||||
|
||||
import core, {
|
||||
AccountRole,
|
||||
Doc,
|
||||
getCurrentAccount,
|
||||
WithLookup,
|
||||
Class,
|
||||
Client,
|
||||
matchQuery,
|
||||
Ref
|
||||
Doc,
|
||||
Ref,
|
||||
WithLookup,
|
||||
getCurrentAccount,
|
||||
matchQuery
|
||||
} from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { Action, ActionGroup, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Action, ActionGroup, ActionIgnore, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view'
|
||||
import view from './plugin'
|
||||
import { FocusSelection, SelectionStore } from './selection'
|
||||
|
||||
@ -126,6 +127,21 @@ export async function getContextActions (
|
||||
return result
|
||||
}
|
||||
|
||||
function getIgnoreActions (ignoreActions: Array<Ref<Action> | ActionIgnore>, doc: Doc): Array<Ref<Action>> {
|
||||
const ignore: Array<Ref<Action>> = []
|
||||
const h = getClient().getHierarchy()
|
||||
for (const a of ignoreActions) {
|
||||
if (typeof a === 'string') {
|
||||
ignore.push(a)
|
||||
} else {
|
||||
if (matchQuery([doc], a.query, a._class, h).length === 1) {
|
||||
ignore.push(a.action)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -140,7 +156,7 @@ export function filterActions (
|
||||
const role = getCurrentAccount().role
|
||||
const clazz = hierarchy.getClass(doc._class)
|
||||
const ignoreActions = hierarchy.as(clazz, view.mixin.IgnoreActions)
|
||||
const ignore: Array<Ref<Action>> = Array.from(ignoreActions?.actions ?? [])
|
||||
const ignore: Array<Ref<Action>> = getIgnoreActions(ignoreActions?.actions ?? [], doc)
|
||||
|
||||
// Collect ignores from parent
|
||||
const ancestors = hierarchy.getAncestors(clazz._id)
|
||||
@ -148,14 +164,14 @@ export function filterActions (
|
||||
for (const cl of ancestors) {
|
||||
const ignoreActions = hierarchy.as(hierarchy.getClassOrInterface(cl), view.mixin.IgnoreActions)
|
||||
if (ignoreActions?.actions !== undefined) {
|
||||
ignore.push(...ignoreActions.actions)
|
||||
ignore.push(...getIgnoreActions(ignoreActions.actions, doc))
|
||||
}
|
||||
}
|
||||
for (const cl of hierarchy.getDescendants(clazz._id)) {
|
||||
if (hierarchy.isMixin(cl) && hierarchy.hasMixin(doc, cl)) {
|
||||
const ignoreActions = hierarchy.as(hierarchy.getClassOrInterface(cl), view.mixin.IgnoreActions)
|
||||
if (ignoreActions?.actions !== undefined) {
|
||||
ignore.push(...ignoreActions.actions)
|
||||
ignore.push(...getIgnoreActions(ignoreActions.actions, doc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,10 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import type { ButtonSize, ButtonKind } from '@hcengineering/ui'
|
||||
import { Label, showPopup, eventToHTMLElement, Button, parseURL } from '@hcengineering/ui'
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import type { AnySvelteComponent, ButtonKind, ButtonSize, IconProps } from '@hcengineering/ui'
|
||||
import { Button, Label, eventToHTMLElement, parseURL, showPopup } from '@hcengineering/ui'
|
||||
import { ComponentType } from 'svelte'
|
||||
import HyperlinkEditorPopup from './HyperlinkEditorPopup.svelte'
|
||||
|
||||
export let placeholder: IntlString
|
||||
@ -27,6 +28,8 @@
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = 'fit-content'
|
||||
export let title: string | undefined
|
||||
export let icon: Asset | AnySvelteComponent | ComponentType | undefined = undefined
|
||||
export let iconProps: IconProps = {}
|
||||
|
||||
let shown: boolean = false
|
||||
</script>
|
||||
@ -36,6 +39,8 @@
|
||||
{size}
|
||||
{justify}
|
||||
{width}
|
||||
{icon}
|
||||
{iconProps}
|
||||
on:click={(ev) => {
|
||||
if (!shown) {
|
||||
showPopup(HyperlinkEditorPopup, { value, editable: !readonly }, eventToHTMLElement(ev), (res) => {
|
||||
|
@ -15,8 +15,8 @@
|
||||
<script lang="ts">
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { themeStore, Label } from '@hcengineering/ui'
|
||||
import { Button, IconArrowRight, IconBlueCheck, IconClose } from '@hcengineering/ui'
|
||||
import { copyTextToClipboard } from '@hcengineering/presentation'
|
||||
import { Button, IconArrowRight, IconBlueCheck, IconClose, IconCopy, Label, themeStore } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import view from '../plugin'
|
||||
|
||||
@ -32,6 +32,9 @@
|
||||
onMount(() => {
|
||||
if (input) input.focus()
|
||||
})
|
||||
const copyLink = (): void => {
|
||||
copyTextToClipboard(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="editor-container buttons-group xsmall-gap">
|
||||
@ -88,6 +91,16 @@
|
||||
focusIndex={4}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
icon={IconCopy}
|
||||
showTooltip={{ label: view.string.CopyToClipboard }}
|
||||
on:click={() => {
|
||||
copyLink()
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
focusIndex={5}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
icon={IconArrowRight}
|
||||
showTooltip={{ label: view.string.Open }}
|
||||
on:click={() => {
|
||||
|
@ -42,6 +42,7 @@
|
||||
export let label: IntlString
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let placeholderIcon: Asset | undefined = undefined
|
||||
export let value: Ref<Doc> | null | undefined
|
||||
export let allowDeselect = false
|
||||
export let titleDeselect: IntlString | undefined = undefined
|
||||
@ -56,6 +57,7 @@
|
||||
export let id: string | undefined = undefined
|
||||
export let searchField: string = 'name'
|
||||
export let docProps: Record<string, any> = {}
|
||||
export let shouldShowAvatar = false
|
||||
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
|
||||
@ -107,8 +109,6 @@
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$: hideIcon = size === 'x-large' || (size === 'large' && kind !== 'link')
|
||||
</script>
|
||||
|
||||
<div {id} bind:this={container} class="min-w-0" class:w-full={width === '100%'} class:h-full={$$slots.content}>
|
||||
@ -121,7 +121,7 @@
|
||||
<Button
|
||||
{focusIndex}
|
||||
width={width ?? 'min-content'}
|
||||
{icon}
|
||||
icon={icon ?? value === undefined ? placeholderIcon : undefined}
|
||||
iconProps={{ size: kind === 'link' || kind === 'regular' ? 'small' : size }}
|
||||
{size}
|
||||
{kind}
|
||||
@ -142,7 +142,7 @@
|
||||
objectId={selected._id}
|
||||
_class={selected._class}
|
||||
value={selected}
|
||||
props={{ ...docProps, disabled: true, noUnderline: true, size: 'x-small', shouldShowAvatar: false }}
|
||||
props={{ ...docProps, disabled: true, noUnderline: true, size: 'x-small', shouldShowAvatar }}
|
||||
/>
|
||||
{:else}
|
||||
<Label {label} />
|
||||
|
@ -76,6 +76,7 @@
|
||||
...getProjection(viewOptions.groupBy, queryNoLookup)
|
||||
}
|
||||
}
|
||||
|
||||
$: docsQuery.query(
|
||||
_class,
|
||||
queryNoLookup,
|
||||
|
@ -98,16 +98,18 @@
|
||||
const autoFoldLimit = 20
|
||||
const defaultLimit = 20
|
||||
const singleCategoryLimit = 50
|
||||
let loading = false
|
||||
$: initialLimit = !lastLevel ? undefined : singleCat ? singleCategoryLimit : defaultLimit
|
||||
$: limit = initialLimit
|
||||
|
||||
$: if (lastLevel) {
|
||||
limiter.add(async () => {
|
||||
docsQuery.query(
|
||||
loading = docsQuery.query(
|
||||
_class,
|
||||
{ ...resultQuery, ...docKeys },
|
||||
(res) => {
|
||||
items = res
|
||||
loading = false
|
||||
},
|
||||
{ ...resultOptions, limit: limit ?? 200 }
|
||||
)
|
||||
@ -159,25 +161,7 @@
|
||||
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(event))
|
||||
}
|
||||
|
||||
let limited: Doc[] = []
|
||||
|
||||
let loading = false
|
||||
let loadingTimeout: any | undefined = undefined
|
||||
|
||||
function update (items: Doc[], limit: number | undefined, index: number): void {
|
||||
clearTimeout(loadingTimeout)
|
||||
if (limited.length > 0 || index * 2 === 0) {
|
||||
limited = limitGroup(items, limit)
|
||||
} else {
|
||||
loading = true
|
||||
loadingTimeout = setTimeout(() => {
|
||||
limited = limitGroup(items, limit)
|
||||
loading = false
|
||||
}, index * 2)
|
||||
}
|
||||
}
|
||||
|
||||
$: update(items, limit, index)
|
||||
$: limited = limitGroup(items, limit)
|
||||
|
||||
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
||||
|
||||
@ -445,6 +429,7 @@
|
||||
{props}
|
||||
{lastCat}
|
||||
{viewOptions}
|
||||
{loading}
|
||||
on:more={() => {
|
||||
if (limit !== undefined) limit += 20
|
||||
}}
|
||||
|
@ -28,6 +28,7 @@
|
||||
IconCollapseArrow,
|
||||
IconMoreH,
|
||||
Label,
|
||||
Loading,
|
||||
defaultBackground,
|
||||
eventToHTMLElement,
|
||||
showPopup,
|
||||
@ -60,6 +61,7 @@
|
||||
export let newObjectProps: (doc: Doc | undefined) => Record<string, any> | undefined
|
||||
|
||||
export let viewOptions: ViewOptions
|
||||
export let loading: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -143,35 +145,41 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if selected.length > 0}
|
||||
<span class="antiSection-header__counter ml-2">
|
||||
<span class="caption-color">
|
||||
({selected.length})
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if limited < itemsProj.length}
|
||||
<div class="antiSection-header__counter flex-row-center mx-2">
|
||||
<span class="caption-color">{limited}</span>
|
||||
<span class="text-xs mx-0-5">/</span>
|
||||
{itemsProj.length}
|
||||
{#if loading}
|
||||
<div class="p-1">
|
||||
<Loading shrink size={'small'} />
|
||||
</div>
|
||||
<ActionIcon
|
||||
size={'small'}
|
||||
icon={IconMoreH}
|
||||
label={ui.string.ShowMore}
|
||||
action={() => {
|
||||
dispatch('more')
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<span class="antiSection-header__counter ml-2">{itemsProj.length}</span>
|
||||
{#if selected.length > 0}
|
||||
<span class="antiSection-header__counter ml-2">
|
||||
<span class="caption-color">
|
||||
({selected.length})
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if limited < itemsProj.length}
|
||||
<div class="antiSection-header__counter flex-row-center mx-2">
|
||||
<span class="caption-color">{limited}</span>
|
||||
<span class="text-xs mx-0-5">/</span>
|
||||
{itemsProj.length}
|
||||
</div>
|
||||
<ActionIcon
|
||||
size={'small'}
|
||||
icon={IconMoreH}
|
||||
label={ui.string.ShowMore}
|
||||
action={() => {
|
||||
dispatch('more')
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<span class="antiSection-header__counter ml-2">{itemsProj.length}</span>
|
||||
{/if}
|
||||
<div class="flex-row-center flex-reverse flex-grow mr-2 gap-2 reverse">
|
||||
{#each extraHeaders ?? [] as extra}
|
||||
<Component is={extra} props={{ ...props, value: category, category: groupByKey, docs: items }} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-row-center flex-reverse flex-grow mr-2 gap-2 reverse">
|
||||
{#each extraHeaders ?? [] as extra}
|
||||
<Component is={extra} props={{ ...props, value: category, category: groupByKey, docs: items }} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#if createItemDialog !== undefined && createItemLabel !== undefined}
|
||||
<div class:on-hover={!mouseOver} class="flex-row-center">
|
||||
|
@ -47,7 +47,8 @@ import {
|
||||
getPanelURI,
|
||||
getPlatformColorForText,
|
||||
locationToUrl,
|
||||
navigate
|
||||
navigate,
|
||||
resolvedLocationStore
|
||||
} from '@hcengineering/ui'
|
||||
import type { BuildModelOptions, Viewlet, ViewletDescriptor } from '@hcengineering/view'
|
||||
import view, { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
||||
@ -562,6 +563,10 @@ export type FixedWidthStore = Record<string, number>
|
||||
|
||||
export const fixedWidthStore = writable<FixedWidthStore>({})
|
||||
|
||||
resolvedLocationStore.subscribe(() => {
|
||||
fixedWidthStore.set({})
|
||||
})
|
||||
|
||||
export function groupBy<T extends Doc> (docs: T[], key: string, categories?: CategoryType[]): Record<any, T[]> {
|
||||
return docs.reduce((storage: { [key: string]: T[] }, item: T) => {
|
||||
let group = getObjectValue(key, item) ?? undefined
|
||||
|
@ -520,11 +520,22 @@ export interface ViewContext {
|
||||
group?: ActionGroup
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ActionIgnore {
|
||||
_class: Ref<Class<Doc>>
|
||||
// Action to be ignored
|
||||
action: Ref<Action>
|
||||
// Document match to ignore if matching at least one document.
|
||||
query: DocumentQuery<Doc>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IgnoreActions extends Class<Doc> {
|
||||
actions: Ref<Action>[]
|
||||
actions: (Ref<Action> | ActionIgnore)[]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -976,6 +987,9 @@ const view = plugin(viewId, {
|
||||
|
||||
// Or list of values to select from
|
||||
values?: { icon?: Asset, label: IntlString, id: number | string }[]
|
||||
|
||||
// If defined, documents will be set into value
|
||||
valueKey?: string
|
||||
}>
|
||||
}
|
||||
})
|
||||
|
@ -406,7 +406,7 @@ export async function confirm (db: Db, productId: string, token: string): Promis
|
||||
async function sendConfirmation (productId: string, account: Account): Promise<void> {
|
||||
const sesURL = getMetadata(accountPlugin.metadata.SES_URL)
|
||||
if (sesURL === undefined || sesURL === '') {
|
||||
throw new Error('Please provide email service url')
|
||||
console.info('Please provide email service url to enable email confirmations.')
|
||||
}
|
||||
const front = getMetadata(accountPlugin.metadata.FrontURL)
|
||||
if (front === undefined || front === '') {
|
||||
@ -443,19 +443,21 @@ async function sendConfirmation (productId: string, account: Account): Promise<v
|
||||
subject = 'Confirm your email address to sign up for ezQMS'
|
||||
}
|
||||
|
||||
const to = account.email
|
||||
await fetch(concatLink(sesURL, '/send'), {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
html,
|
||||
subject,
|
||||
to
|
||||
if (sesURL !== undefined) {
|
||||
const to = account.email
|
||||
await fetch(concatLink(sesURL, '/send'), {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
html,
|
||||
subject,
|
||||
to
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -274,7 +274,13 @@ export async function cloneWorkspace (
|
||||
docs = await sourceConnection.loadDocs(c, needRetrieve)
|
||||
if (clearTime) {
|
||||
docs = docs.map((p) => {
|
||||
if (sourceConnection.getHierarchy().isDerived(p._class, core.class.TxCollectionCUD)) {
|
||||
let collectionCud = false
|
||||
try {
|
||||
collectionCud = sourceConnection.getHierarchy().isDerived(p._class, core.class.TxCollectionCUD)
|
||||
} catch (err: any) {
|
||||
console.log(err)
|
||||
}
|
||||
if (collectionCud) {
|
||||
return {
|
||||
...p,
|
||||
createdBy: core.account.System,
|
||||
|
@ -141,7 +141,7 @@ abstract class MongoAdapterBase implements DbAdapter {
|
||||
if (!classes.includes(translated._class)) {
|
||||
translated._class = { $in: classes }
|
||||
}
|
||||
} else if (typeof translated._class === 'object') {
|
||||
} else if (typeof translated._class === 'object' && translated._class !== null) {
|
||||
let descendants: Ref<Class<Doc>>[] = classes
|
||||
|
||||
if (Array.isArray(translated._class.$in)) {
|
||||
@ -149,7 +149,7 @@ abstract class MongoAdapterBase implements DbAdapter {
|
||||
descendants = translated._class.$in.filter((c: Ref<Class<Doc>>) => classesIds.has(c))
|
||||
}
|
||||
|
||||
if (Array.isArray(translated._class.$nin)) {
|
||||
if (translated._class != null && Array.isArray(translated._class.$nin)) {
|
||||
const excludedClassesIds = new Set<Ref<Class<Doc>>>(translated._class.$nin)
|
||||
descendants = descendants.filter((c) => !excludedClassesIds.has(c))
|
||||
}
|
||||
|
@ -209,10 +209,11 @@ export async function upgradeModel (
|
||||
} catch (err: any) {}
|
||||
}
|
||||
|
||||
const migrateClient = new MigrateClientImpl(db, hierarchy, modelDb)
|
||||
const migrateClient = new MigrateClientImpl(db, hierarchy, modelDb, logger)
|
||||
for (const op of migrateOperations) {
|
||||
logger.log(`${workspaceId.name}: migrate:`, op[0])
|
||||
const t = Date.now()
|
||||
await op[1].migrate(migrateClient, logger)
|
||||
logger.log(`${workspaceId.name}: migrate:`, op[0], Date.now() - t)
|
||||
}
|
||||
|
||||
logger.log(`${workspaceId.name}: Apply upgrade operations`)
|
||||
@ -223,8 +224,9 @@ export async function upgradeModel (
|
||||
await createUpdateIndexes(connection, db, logger)
|
||||
|
||||
for (const op of migrateOperations) {
|
||||
logger.log(`${workspaceId.name}: upgrade:`, op[0])
|
||||
const t = Date.now()
|
||||
await op[1].upgrade(connection, logger)
|
||||
logger.log(`${workspaceId.name}: upgrade:`, op[0], Date.now() - t)
|
||||
}
|
||||
|
||||
await connection.close()
|
||||
@ -279,7 +281,7 @@ async function createUpdateIndexes (connection: CoreClient, db: Db, logger: Mode
|
||||
bb.push(vv)
|
||||
}
|
||||
if (bb.length > 0) {
|
||||
logger.log('created indexes', d, bb)
|
||||
logger.log('created indexes', d, JSON.stringify(bb))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,14 +9,14 @@ import {
|
||||
Ref,
|
||||
SortingOrder
|
||||
} from '@hcengineering/core'
|
||||
import { MigrateUpdate, MigrationClient, MigrationResult } from '@hcengineering/model'
|
||||
import { MigrateUpdate, MigrationClient, MigrationResult, ModelLogger } from '@hcengineering/model'
|
||||
import { Db, Document, Filter, Sort, UpdateFilter } from 'mongodb'
|
||||
|
||||
/**
|
||||
* Upgrade client implementation.
|
||||
*/
|
||||
export class MigrateClientImpl implements MigrationClient {
|
||||
constructor (readonly db: Db, readonly hierarchy: Hierarchy, readonly model: ModelDb) {}
|
||||
constructor (readonly db: Db, readonly hierarchy: Hierarchy, readonly model: ModelDb, readonly logger: ModelLogger) {}
|
||||
|
||||
private translateQuery<T extends Doc>(query: DocumentQuery<T>): Filter<Document> {
|
||||
const translated: any = {}
|
||||
@ -65,15 +65,22 @@ export class MigrateClientImpl implements MigrationClient {
|
||||
query: DocumentQuery<T>,
|
||||
operations: MigrateUpdate<T>
|
||||
): Promise<MigrationResult> {
|
||||
if (isOperator(operations)) {
|
||||
const result = await this.db
|
||||
.collection(domain)
|
||||
.updateMany(this.translateQuery(query), { ...operations } as unknown as UpdateFilter<Document>)
|
||||
const t = Date.now()
|
||||
try {
|
||||
if (isOperator(operations)) {
|
||||
const result = await this.db
|
||||
.collection(domain)
|
||||
.updateMany(this.translateQuery(query), { ...operations } as unknown as UpdateFilter<Document>)
|
||||
|
||||
return { matched: result.matchedCount, updated: result.modifiedCount }
|
||||
} else {
|
||||
const result = await this.db.collection(domain).updateMany(this.translateQuery(query), { $set: operations })
|
||||
return { matched: result.matchedCount, updated: result.modifiedCount }
|
||||
return { matched: result.matchedCount, updated: result.modifiedCount }
|
||||
} else {
|
||||
const result = await this.db.collection(domain).updateMany(this.translateQuery(query), { $set: operations })
|
||||
return { matched: result.matchedCount, updated: result.modifiedCount }
|
||||
}
|
||||
} finally {
|
||||
if (Date.now() - t > 1000) {
|
||||
this.logger.log(`update${Date.now() - t > 5000 ? 'slow' : ''}`, domain, query, Date.now() - t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,6 +105,7 @@ export class MigrateClientImpl implements MigrationClient {
|
||||
query: DocumentQuery<T>,
|
||||
targetDomain: Domain
|
||||
): Promise<MigrationResult> {
|
||||
this.logger.log('move', sourceDomain, query)
|
||||
const q = this.translateQuery(query)
|
||||
const cursor = this.db.collection(sourceDomain).find<T>(q)
|
||||
const target = this.db.collection(targetDomain)
|
||||
|
@ -90,7 +90,8 @@ services:
|
||||
- SERVER_SECRET=secret
|
||||
- ELASTIC_URL=http://elastic:9200
|
||||
- MONGO_URL=mongodb://mongodb:27018
|
||||
- METRICS_CONSOLE=true
|
||||
- METRICS_CONSOLE=false
|
||||
- METRICS_FILE=metrics.txt
|
||||
- MINIO_ENDPOINT=minio
|
||||
- MINIO_ACCESS_KEY=minioadmin
|
||||
- MINIO_SECRET_KEY=minioadmin
|
||||
|
Loading…
Reference in New Issue
Block a user