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