diff --git a/.vscode/launch.json b/.vscode/launch.json index 4f5cd94c89..db975903dc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -57,6 +57,7 @@ // "RETRANSLATE_TOKEN": "" }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], + "runtimeVersion": "20", "showAsyncStacks": true, "sourceMaps": true, "cwd": "${workspaceRoot}/pods/server", @@ -154,7 +155,7 @@ "name": "Debug tool upgrade", "type": "node", "request": "launch", - "args": ["src/index.ts", "upgrade"], + "args": ["src/__start.ts", "upgrade"], "env": { "SERVER_SECRET": "secret", "MINIO_ACCESS_KEY": "minioadmin", diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index bebbbc21d7..887a09d175 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -18075,6 +18075,7 @@ packages: eslint-plugin-n: 15.7.0(eslint@8.56.0) eslint-plugin-promise: 6.1.1(eslint@8.56.0) eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2) + fast-equals: 2.0.4 jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12) @@ -18824,6 +18825,7 @@ packages: eslint-plugin-n: 15.7.0(eslint@8.56.0) eslint-plugin-promise: 6.1.1(eslint@8.56.0) eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2) + fast-equals: 2.0.4 jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) prettier: 3.2.5 prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12) diff --git a/models/all/src/migration.ts b/models/all/src/migration.ts index 19d91dabc9..8b279a7c2c 100644 --- a/models/all/src/migration.ts +++ b/models/all/src/migration.ts @@ -40,6 +40,7 @@ import { calendarOperation } from '@hcengineering/model-calendar' import { timeOperation } from '@hcengineering/model-time' import { activityOperation } from '@hcengineering/model-activity' import { activityServerOperation } from '@hcengineering/model-server-activity' +import { documentOperation } from '@hcengineering/model-document' export const migrateOperations: [string, MigrateOperation][] = [ ['core', coreOperation], @@ -66,6 +67,7 @@ export const migrateOperations: [string, MigrateOperation][] = [ ['inventiry', inventoryOperation], ['time', timeOperation], ['activityServer', activityServerOperation], + ['document', documentOperation], // We should call it after activityServer and chunter ['notification', notificationOperation] ] diff --git a/models/board/src/index.ts b/models/board/src/index.ts index e0b91e6c2f..832a8c0a22 100644 --- a/models/board/src/index.ts +++ b/models/board/src/index.ts @@ -505,6 +505,7 @@ export function createModel (builder: Builder): void { description: board.string.ManageBoardStatuses, icon: board.icon.Board, baseClass: board.class.Board, + availablePermissions: [core.permission.DeleteObject], allowedTaskTypeDescriptors: [board.descriptors.Card] }, board.descriptors.BoardType diff --git a/models/board/src/migration.ts b/models/board/src/migration.ts index 28f01fd097..c99f1161b0 100644 --- a/models/board/src/migration.ts +++ b/models/board/src/migration.ts @@ -59,6 +59,7 @@ async function createDefaultProjectType (tx: TxOperations): Promise> } +@Model(core.class.TypedSpace, core.class.Space) +@UX(core.string.TypedSpace, undefined, undefined, 'name') +export class TTypedSpace extends TSpace implements TypedSpace { + @Prop(TypeRef(core.class.SpaceType), core.string.SpaceType) + type!: Ref +} + +@Model(core.class.SpaceTypeDescriptor, core.class.Doc, DOMAIN_MODEL) +export class TSpaceTypeDescriptor extends TDoc implements SpaceTypeDescriptor { + name!: IntlString + description!: IntlString + icon!: Asset + baseClass!: Ref> + availablePermissions!: Ref[] +} + +@Model(core.class.SpaceType, core.class.Doc, DOMAIN_MODEL) +@UX(core.string.SpaceType, undefined, undefined, 'name') +export class TSpaceType extends TDoc implements SpaceType { + @Prop(TypeString(), core.string.Name) + @Index(IndexKind.FullText) + name!: string + + @Prop(TypeString(), core.string.ShortDescription) + shortDescription?: string + + @Prop(TypeRef(core.class.SpaceTypeDescriptor), core.string.Descriptor) + descriptor!: Ref + + @Prop(TypeRef(core.class.Class), core.string.TargetClass) + targetClass!: Ref> + + @Prop(Collection(core.class.Role), core.string.Roles) + roles!: CollectionSize +} + +@Model(core.class.Role, core.class.AttachedDoc, DOMAIN_MODEL) +@UX(core.string.Role, undefined, undefined, 'name') +export class TRole extends TAttachedDoc implements Role { + @Prop(TypeRef(core.class.SpaceType), core.string.AttachedTo) + @Index(IndexKind.Indexed) + @Hidden() + declare attachedTo: Ref + + @Prop(TypeRef(core.class.SpaceType), core.string.AttachedToClass) + @Index(IndexKind.Indexed) + @Hidden() + declare attachedToClass: Ref> + + @Prop(TypeString(), core.string.Collection) + @Hidden() + override collection: 'roles' = 'roles' + + @Prop(TypeString(), core.string.Name) + @Index(IndexKind.FullText) + name!: string + + @Prop(ArrOf(TypeRef(core.class.Permission)), core.string.Permission) + permissions!: Ref[] +} + +@Model(core.class.Permission, core.class.Doc, DOMAIN_MODEL) +@UX(core.string.Permission) +export class TPermission extends TDoc implements Permission { + label!: IntlString + description?: IntlString + icon?: Asset +} + @Model(core.class.Account, core.class.Doc, DOMAIN_MODEL) @UX(core.string.Account) export class TAccount extends TDoc implements Account { diff --git a/models/document/src/index.ts b/models/document/src/index.ts index 12a02f062b..2f9b9b959f 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -40,7 +40,7 @@ import { } from '@hcengineering/model' import attachment, { TAttachment } from '@hcengineering/model-attachment' import chunter from '@hcengineering/model-chunter' -import core, { TAttachedDoc, TSpace } from '@hcengineering/model-core' +import core, { TAttachedDoc, TTypedSpace } from '@hcengineering/model-core' import { createPublicLinkAction } from '@hcengineering/model-guest' import { generateClassNotificationTypes } from '@hcengineering/model-notification' import preference, { TPreference } from '@hcengineering/model-preference' @@ -78,6 +78,11 @@ export class TDocument extends TAttachedDoc implements Document { @Hidden() declare attachedToClass: Ref> + @Prop(TypeRef(core.class.Space), core.string.Space) + @Index(IndexKind.Indexed) + @Hidden() + declare space: Ref + @Prop(TypeString(), core.string.Collection) @Hidden() override collection: 'children' = 'children' @@ -129,6 +134,11 @@ export class TDocumentSnapshot extends TAttachedDoc implements DocumentSnapshot @Prop(TypeRef(core.class.Class), core.string.AttachedToClass) declare attachedToClass: Ref> + @Prop(TypeRef(core.class.Space), core.string.Space) + @Index(IndexKind.Indexed) + @Hidden() + declare space: Ref + @Prop(TypeString(), core.string.Collection) @Hidden() override collection: 'snapshots' = 'snapshots' @@ -148,13 +158,26 @@ export class TSavedDocument extends TPreference implements SavedDocument { declare attachedTo: Ref } -@Model(document.class.Teamspace, core.class.Space) +@Model(document.class.Teamspace, core.class.TypedSpace) @UX(document.string.Teamspace, document.icon.Teamspace, 'Teamspace', 'name') -export class TTeamspace extends TSpace implements Teamspace {} +export class TTeamspace extends TTypedSpace implements Teamspace {} function defineTeamspace (builder: Builder): void { builder.createModel(TTeamspace) + builder.createDoc( + core.class.SpaceTypeDescriptor, + core.space.Model, + { + name: document.string.DocumentApplication, + description: document.string.Description, + icon: document.icon.Document, + baseClass: document.class.Teamspace, + availablePermissions: [core.permission.DeleteObject] + }, + document.descriptor.TeamspaceType + ) + // Navigator builder.mixin(document.class.Teamspace, core.class.Class, view.mixin.SpacePresenter, { diff --git a/models/document/src/migration.ts b/models/document/src/migration.ts index 329fcced51..8c8e439cb2 100644 --- a/models/document/src/migration.ts +++ b/models/document/src/migration.ts @@ -25,6 +25,8 @@ import { import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment' import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import { type Asset } from '@hcengineering/platform' +import { createSpaceType } from '@hcengineering/setting' + import document, { documentId, DOMAIN_DOCUMENT } from './index' async function createSpace (tx: TxOperations): Promise { @@ -238,6 +240,40 @@ async function setNoParent (client: MigrationClient): Promise { ) } +async function createDefaultTeamspaceType (tx: TxOperations): Promise { + const exist = await tx.findOne(core.class.SpaceType, { _id: document.spaceType.DefaultTeamspaceType }) + const deleted = await tx.findOne(core.class.TxRemoveDoc, { + objectId: document.spaceType.DefaultTeamspaceType + }) + + if (exist === undefined && deleted === undefined) { + await createSpaceType( + tx, + { + name: 'Default teamspace type', + descriptor: document.descriptor.TeamspaceType, + roles: 0 + }, + document.spaceType.DefaultTeamspaceType + ) + } +} + +async function migrateTeamspaces (client: MigrationClient): Promise { + await client.update( + DOMAIN_SPACE, + { + _class: document.class.Teamspace, + type: { $exists: false } + }, + { + $set: { + type: document.spaceType.DefaultTeamspaceType + } + } + ) +} + export const documentOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await setNoParent(client) @@ -263,10 +299,12 @@ export const documentOperation: MigrateOperation = { func: migrateDocumentIcons } ]) + await migrateTeamspaces(client) }, async upgrade (client: MigrationUpgradeClient): Promise { const tx = new TxOperations(client, core.account.System) await createSpace(tx) + await createDefaultTeamspaceType(tx) } } diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index 9f29fe88bb..a1756beacc 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -672,6 +672,7 @@ export function createModel (builder: Builder): void { description: lead.string.ManageFunnelStatuses, icon: lead.icon.LeadApplication, baseClass: lead.class.Funnel, + availablePermissions: [core.permission.DeleteObject], allowedTaskTypeDescriptors: [lead.descriptors.Lead] }, lead.descriptors.FunnelType diff --git a/models/lead/src/migration.ts b/models/lead/src/migration.ts index a99866112d..c0f0f2b58f 100644 --- a/models/lead/src/migration.ts +++ b/models/lead/src/migration.ts @@ -39,6 +39,7 @@ async function createSpace (tx: TxOperations): Promise { descriptor: lead.descriptors.FunnelType, description: '', tasks: [], + roles: 0, classic: false }, [ diff --git a/models/lead/src/plugin.ts b/models/lead/src/plugin.ts index cc03d12dfc..772ec8cb70 100644 --- a/models/lead/src/plugin.ts +++ b/models/lead/src/plugin.ts @@ -21,7 +21,7 @@ import lead from '@hcengineering/lead-resources/src/plugin' import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' import type { IntlString } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform' -import { type TaskTypeDescriptor, type ProjectType } from '@hcengineering/task' +import { type TaskTypeDescriptor } from '@hcengineering/task' import type { AnyComponent } from '@hcengineering/ui/src/types' import { type Action, type ActionCategory, type Viewlet } from '@hcengineering/view' @@ -48,9 +48,6 @@ export default mergeIds(leadId, lead, { space: { DefaultFunnel: '' as Ref }, - template: { - DefaultFunnel: '' as Ref - }, viewlet: { TableCustomer: '' as Ref, TableLead: '' as Ref, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 23f9e24a6c..e924f9aad1 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -1709,6 +1709,7 @@ export function createModel (builder: Builder): void { icon: recruit.icon.RecruitApplication, editor: recruit.component.VacancyTemplateEditor, baseClass: recruit.class.Vacancy, + availablePermissions: [core.permission.DeleteObject], allowedTaskTypeDescriptors: [recruit.descriptors.Application] }, recruit.descriptors.VacancyType diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index 2b38cee9dd..a9ca8a19b8 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -157,6 +157,7 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise +} + +@Mixin(setting.mixin.SpaceTypeCreator, core.class.Class) +export class TSpaceTypeCreator extends TClass implements SpaceTypeCreator { + extraComponent!: AnyComponent +} + export function createModel (builder: Builder): void { builder.createModel( TIntegration, @@ -114,7 +128,9 @@ export function createModel (builder: Builder): void { TEditable, TUserMixin, TInviteSettings, - TWorkspaceSetting + TWorkspaceSetting, + TSpaceTypeEditor, + TSpaceTypeCreator ) builder.mixin(setting.class.Integration, core.class.Class, notification.mixin.ClassCollaborators, { @@ -560,4 +576,56 @@ export function createModel (builder: Builder): void { }, setting.ids.IntegrationDisabledNotification ) + + builder.createDoc( + setting.class.SettingsCategory, + core.space.Model, + { + name: 'spaceTypes', + label: setting.string.SpaceTypes, + icon: setting.icon.Privacy, // TODO: update icon. Where is it displayed? + component: setting.component.ManageSpaceTypeContent, + extraComponents: { + navigation: setting.component.ManageSpaceTypes, + tools: setting.component.ManageSpaceTypesTools + }, + group: 'settings-editor', + secured: false, + order: 6000, + expandable: true + }, + setting.ids.ManageSpaces + ) + + builder.mixin(core.class.SpaceType, core.class.Class, setting.mixin.SpaceTypeEditor, { + sections: [ + { + id: 'general', + label: setting.string.General, + component: setting.component.SpaceTypeGeneralSectionEditor, + withoutContainer: true + }, + { + id: 'properties', + label: setting.string.Properties, + component: setting.component.SpaceTypePropertiesSectionEditor + }, + { + id: 'roles', + label: setting.string.Roles, + component: setting.component.SpaceTypeRolesSectionEditor + } + ], + subEditors: { + roles: setting.component.RoleEditor + } + }) + + builder.mixin(core.class.SpaceTypeDescriptor, core.class.Class, view.mixin.ObjectPresenter, { + presenter: setting.component.SpaceTypeDescriptorPresenter + }) + + builder.mixin(core.class.Permission, core.class.Class, view.mixin.ObjectPresenter, { + presenter: setting.component.PermissionPresenter + }) } diff --git a/models/task/src/index.ts b/models/task/src/index.ts index 1a1118bd89..c133176d95 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -53,7 +53,15 @@ import { } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import chunter from '@hcengineering/model-chunter' -import core, { DOMAIN_SPACE, TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core' +import core, { + DOMAIN_SPACE, + TAttachedDoc, + TClass, + TDoc, + TSpaceType, + TSpaceTypeDescriptor, + TTypedSpace +} from '@hcengineering/model-core' import view, { classPresenter, createAction, @@ -173,18 +181,20 @@ export class TProjectTypeClass extends TClass implements ProjectTypeClass { projectType!: Ref } -@Model(task.class.Project, core.class.Space) -export class TProject extends TSpace implements Project { +@Model(task.class.Project, core.class.TypedSpace) +export class TProject extends TTypedSpace implements Project { @Prop(TypeRef(task.class.ProjectType), task.string.ProjectType) - type!: Ref + declare type: Ref } -@Model(task.class.ProjectType, core.class.Space) -export class TProjectType extends TSpace implements ProjectType { - shortDescription?: string - +@Model(task.class.ProjectType, core.class.SpaceType) +export class TProjectType extends TSpaceType implements ProjectType { @Prop(TypeRef(task.class.ProjectTypeDescriptor), getEmbeddedLabel('Descriptor')) - descriptor!: Ref + declare descriptor: Ref + + @Prop(TypeString(), task.string.Description) + @Index(IndexKind.FullText) + description!: string @Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Tasks')) tasks!: Ref[] @@ -193,13 +203,13 @@ export class TProjectType extends TSpace implements ProjectType { statuses!: ProjectStatus[] @Prop(TypeRef(core.class.Class), getEmbeddedLabel('Target Class')) - targetClass!: Ref> + declare targetClass: Ref> @Prop(TypeBoolean(), getEmbeddedLabel('Classic')) classic!: boolean } -@Model(task.class.TaskType, core.class.Doc, DOMAIN_TASK) +@Model(task.class.TaskType, core.class.Doc, DOMAIN_MODEL) export class TTaskType extends TDoc implements TaskType { @Prop(TypeString(), getEmbeddedLabel('Name')) name!: string @@ -232,15 +242,12 @@ export class TTaskType extends TDoc implements TaskType { statusCategories!: Ref[] } -@Model(task.class.ProjectTypeDescriptor, core.class.Doc, DOMAIN_MODEL) -export class TProjectTypeDescriptor extends TDoc implements ProjectTypeDescriptor { - name!: IntlString - description!: IntlString - icon!: Asset +@Model(task.class.ProjectTypeDescriptor, core.class.SpaceTypeDescriptor, DOMAIN_MODEL) +export class TProjectTypeDescriptor extends TSpaceTypeDescriptor implements ProjectTypeDescriptor { editor?: AnyComponent allowedClassic?: boolean allowedTaskTypeDescriptors?: Ref[] // if undefined we allow all possible - baseClass!: Ref> + declare baseClass: Ref> } @Model(task.class.Sequence, core.class.Doc, DOMAIN_KANBAN) @@ -540,25 +547,49 @@ export function createModel (builder: Builder): void { value: true }) - builder.createDoc( - setting.class.SettingsCategory, - core.space.Model, - { - name: 'statuses', - label: task.string.ManageProjects, - icon: task.icon.ManageTemplates, - component: task.component.ManageProjectsContent, - extraComponents: { - navigation: task.component.ManageProjects, - tools: task.component.ManageProjectsTools + builder.mixin(task.class.ProjectTypeDescriptor, core.class.Class, setting.mixin.SpaceTypeCreator, { + extraComponent: task.component.CreateProjectType + }) + + builder.mixin(task.class.ProjectType, core.class.Class, setting.mixin.SpaceTypeEditor, { + sections: [ + { + id: 'general', + label: setting.string.General, + component: task.component.ProjectTypeGeneralSectionEditor, + withoutContainer: true }, - group: 'settings-editor', - secured: false, - order: 6000, - expandable: true - }, - task.ids.ManageProjects - ) + { + id: 'properties', + label: setting.string.Properties, + component: setting.component.SpaceTypePropertiesSectionEditor + }, + { + id: 'roles', + label: setting.string.Roles, + component: setting.component.SpaceTypeRolesSectionEditor + }, + { + id: 'taskTypes', + label: setting.string.TaskTypes, + component: task.component.ProjectTypeTasksTypeSectionEditor + }, + { + id: 'automations', + label: setting.string.Automations, + component: task.component.ProjectTypeAutomationsSectionEditor + }, + { + id: 'collections', + label: setting.string.Collections, + component: task.component.ProjectTypeCollectionsSectionEditor + } + ], + subEditors: { + taskTypes: task.component.TaskTypeEditor, + roles: setting.component.RoleEditor + } + }) createPublicLinkAction(builder, task.class.Task, task.action.PublicLink) } @@ -589,8 +620,7 @@ export async function fixTaskTypes ( throw new Error('category is not found in model') } - const projectTypes = await client.find(DOMAIN_SPACE, { - _class: task.class.ProjectType, + const projectTypes = await client.model.findAll(task.class.ProjectType, { descriptor }) const baseClassClass = client.hierarchy.getClass(categoryObj.baseClass) @@ -751,7 +781,7 @@ export async function fixTaskTypes ( parent: t._id, _id: taskTypeId, _class: task.class.TaskType, - space: t._id, + space: core.space.Model, statuses: dStatuses, modifiedBy: core.account.System, modifiedOn: Date.now(), diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index a95bfd7e2c..1c674e1605 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -13,7 +13,17 @@ // limitations under the License. // -import { ClassifierKind, TxOperations, toIdMap, type Class, type Doc, type Ref } from '@hcengineering/core' +import { + ClassifierKind, + TxOperations, + toIdMap, + type Class, + type Doc, + type Ref, + type TxCreateDoc, + generateId, + DOMAIN_TX +} from '@hcengineering/core' import { createOrUpdate, tryMigrate, @@ -25,7 +35,7 @@ import { import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import tags from '@hcengineering/model-tags' import { getEmbeddedLabel } from '@hcengineering/platform' -import { taskId, type TaskType } from '@hcengineering/task' +import { type ProjectType, taskId, type TaskType } from '@hcengineering/task' import { DOMAIN_TASK } from '.' import task from './plugin' @@ -167,6 +177,79 @@ async function fixProjectTypeMissingClass (client: MigrationUpgradeClient): Prom } } +async function migrateProjectTypes (client: MigrationClient): Promise { + const oldProjectTypes = await client.find(DOMAIN_SPACE, { _class: task.class.ProjectType }) + + for (const pt of oldProjectTypes) { + // Create the project type in model instead + const tx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectClass: pt._class, + objectSpace: core.space.Model, + objectId: pt._id, + attributes: { + descriptor: pt.descriptor, + tasks: pt.tasks, + statuses: pt.statuses, + targetClass: pt.targetClass, + classic: pt.classic, + name: pt.name, + description: pt.description, + shortDescription: pt.shortDescription, + roles: pt.roles ?? [] + }, + modifiedOn: pt.modifiedOn, + createdBy: pt.createdBy, + createdOn: pt.createdOn, + modifiedBy: pt.modifiedBy + } + await client.create(DOMAIN_TX, tx) + + // Remove old project type from spaces + await client.delete(DOMAIN_SPACE, pt._id) + } +} + +async function migrateTaskTypes (client: MigrationClient): Promise { + const oldTaskTypes = await client.find(DOMAIN_TASK, { _class: task.class.TaskType }) + + for (const tt of oldTaskTypes) { + // Create the task type in model instead + const tx: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectClass: tt._class, + objectSpace: core.space.Model, + objectId: tt._id, + attributes: { + parent: tt.parent, + descriptor: tt.descriptor, + name: tt.name, + kind: tt.kind, + ofClass: tt.ofClass, + targetClass: tt.targetClass, + statuses: tt.statuses, + statusClass: tt.statusClass, + statusCategories: tt.statusCategories, + allowedAsChildOf: tt.allowedAsChildOf, + icon: tt.icon, + color: tt.color + }, + modifiedOn: tt.modifiedOn, + createdBy: tt.createdBy, + createdOn: tt.createdOn, + modifiedBy: tt.modifiedBy + } + await client.create(DOMAIN_TX, tx) + + // Remove old task type from task + await client.delete(DOMAIN_TASK, tt._id) + } +} + export const taskOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await tryMigrate(client, taskId, [ @@ -201,6 +284,8 @@ export const taskOperation: MigrateOperation = { } } ]) + await migrateTaskTypes(client) + await migrateProjectTypes(client) }, async upgrade (client: MigrationUpgradeClient): Promise { const tx = new TxOperations(client, core.account.System) diff --git a/models/task/src/plugin.ts b/models/task/src/plugin.ts index 82e240d8f3..a97f343a2f 100644 --- a/models/task/src/plugin.ts +++ b/models/task/src/plugin.ts @@ -61,10 +61,7 @@ export default mergeIds(taskId, task, { TaskTypePresenter: '' as AnyComponent, ProjectTypePresenter: '' as AnyComponent, TaskTypeClassPresenter: '' as AnyComponent, - ProjectTypeClassPresenter: '' as AnyComponent, - ManageProjects: '' as AnyComponent, - ManageProjectsTools: '' as AnyComponent, - ManageProjectsContent: '' as AnyComponent + ProjectTypeClassPresenter: '' as AnyComponent }, space: { TasksPublic: '' as Ref diff --git a/models/tracker/src/actions.ts b/models/tracker/src/actions.ts index 5ec5f55887..916998206a 100644 --- a/models/tracker/src/actions.ts +++ b/models/tracker/src/actions.ts @@ -182,6 +182,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId: mode: ['context', 'browser'], group: 'remove' }, + visibilityTester: view.function.CanDeleteObject, override: [view.action.Delete] }, tracker.action.DeleteIssue @@ -709,7 +710,8 @@ export function createActions (builder: Builder, issuesId: string, componentsId: category: tracker.category.Tracker, input: 'any', target: tracker.class.Milestone, - context: { mode: ['context', 'browser'], group: 'remove' } + context: { mode: ['context', 'browser'], group: 'remove' }, + visibilityTester: view.function.CanDeleteObject }, tracker.action.DeleteMilestone ) diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index c99e6d36ac..66a9ad34a4 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -687,6 +687,7 @@ export function createModel (builder: Builder): void { description: tracker.string.ManageWorkflowStatuses, icon: task.icon.Task, baseClass: tracker.class.Project, + availablePermissions: [core.permission.DeleteObject], allowedClassic: true, allowedTaskTypeDescriptors: [tracker.descriptors.Issue] }, diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index 9cdf3dfeb2..016f0132a8 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -66,6 +66,7 @@ async function createDefaultProject (tx: TxOperations): Promise { descriptor: tracker.descriptors.ProjectType, description: '', tasks: [], + roles: 0, classic: true }, [ diff --git a/models/view/src/index.ts b/models/view/src/index.ts index e4e09d0d01..3570b7caa5 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -624,7 +624,8 @@ export function createModel (builder: Builder): void { category: view.category.General, input: 'any', target: core.class.Doc, - context: { mode: ['context', 'browser'], group: 'remove' } + context: { mode: ['context', 'browser'], group: 'remove' }, + visibilityTester: view.function.CanDeleteObject }, view.action.Delete ) diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 97720550e7..7c33b052c9 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -13,8 +13,8 @@ // limitations under the License. // -import { type Ref } from '@hcengineering/core' -import { type IntlString, mergeIds } from '@hcengineering/platform' +import { type Doc, type Ref } from '@hcengineering/core' +import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform' import { type AnyComponent } from '@hcengineering/ui/src/types' import { type FilterFunction, type ViewAction, type ViewCategoryAction, viewId } from '@hcengineering/view' import { type PresentationMiddlewareFactory } from '@hcengineering/presentation/src/pipeline' @@ -126,7 +126,8 @@ export default mergeIds(viewId, view, { FilterDateNextMonth: '' as FilterFunction, FilterDateNotSpecified: '' as FilterFunction, FilterDateCustom: '' as FilterFunction, - ShowEmptyGroups: '' as ViewCategoryAction + ShowEmptyGroups: '' as ViewCategoryAction, + CanDeleteObject: '' as Resource<(doc?: Doc | Doc[]) => Promise> }, pipeline: { PresentationMiddleware: '' as Ref, diff --git a/packages/core/lang/en.json b/packages/core/lang/en.json index ca27201eec..3e11bf91e4 100644 --- a/packages/core/lang/en.json +++ b/packages/core/lang/en.json @@ -2,6 +2,8 @@ "string": { "Id": "Id", "Space": "Space", + "TypedSpace": "Typed space", + "SpaceType": "Space type", "Modified": "Modified", "ModifiedDate": "Modified date", "ModifiedBy": "Modified by", @@ -10,6 +12,11 @@ "AttachedToClass": "Attached to class", "Name": "Name", "Description": "Description", + "ShortDescription": "Short description", + "Descriptor": "Descriptor", + "TargetClass": "Target class", + "Role": "Role", + "Roles": "Roles", "Private": "Private", "Archived": "Archived", "ClassLabel": "Type", @@ -35,6 +42,11 @@ "Status": "Status", "StatusCategory": "Status category", "Account": "Account", - "Rank": "Rank" + "Rank": "Rank", + "Permission": "Permission", + "CreateObject": "Create object", + "UpdateObject": "Update object", + "DeleteObject": "Delete object", + "DeleteObjectDescription": "Grants users ability to delete objects in the space" } } diff --git a/packages/core/lang/ru.json b/packages/core/lang/ru.json index a67627519d..b4e71ef504 100644 --- a/packages/core/lang/ru.json +++ b/packages/core/lang/ru.json @@ -2,6 +2,8 @@ "string": { "Id": "Id", "Space": "Пространство", + "TypedSpace": "Типизированное пространство", + "SpaceType": "Тип пространства", "Modified": "Изменено", "ModifiedDate": "Дата изменения", "ModifiedBy": "Изменен", @@ -10,6 +12,11 @@ "AttachedToClass": "Прикреплен к классу", "Name": "Название", "Description": "Описание", + "ShortDescription": "Короткое описание", + "Descriptor": "Дескриптор", + "TargetClass": "Целевой класс", + "Role": "Роль", + "Roles": "Роли", "Private": "Личный", "Archived": "Архивный", "ClassLabel": "Тип", @@ -35,6 +42,11 @@ "Status": "Статус", "StatusCategory": "Категория статуса", "Account": "Аккаунт", - "Rank": "Ранг" + "Rank": "Ранг", + "Permission": "Разрешение", + "CreateObject": "Создавать объект", + "UpdateObject": "Обновлять объект", + "DeleteObject": "Удалять объект", + "DeleteObjectDescription": "Дает пользователям разрешение удалять объекты в пространстве" } } diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index f510eec88b..f92c8d9801 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -362,6 +362,66 @@ export interface Space extends Doc { archived: boolean } +/** + * @public + * + * Space with custom configured type + */ +export interface TypedSpace extends Space { + type: Ref +} + +/** + * @public + * + * Is used to describe "types" for space type + */ +export interface SpaceTypeDescriptor extends Doc { + name: IntlString + description: IntlString + icon: Asset + baseClass: Ref> // Child class of Space for which the space type can be defined + availablePermissions: Ref[] +} + +/** + * @public + * + * Customisable space type allowing to configure space roles and permissions within them + */ +export interface SpaceType extends Doc { + name: string + shortDescription?: string + descriptor: Ref + targetClass: Ref> // A dynamic mixin for Spaces to hold custom attributes and roles assignment of the space type + roles: CollectionSize +} + +/** + * @public + * Role defines permissions for employees assigned to this role within the space + */ +export interface Role extends AttachedDoc { + name: string + permissions: Ref[] +} + +/** + * @public + * Defines assignment of employees to a role within a space + */ +export type RolesAssignment = Record, Ref[] | undefined> + +/** + * @public + * Permission is a basic access control item in the system + */ +export interface Permission extends Doc { + label: IntlString + description?: IntlString + icon?: Asset +} + /** * @public */ diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 139abbcabc..1746efc80f 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -38,14 +38,19 @@ import type { Markup, MigrationState, Obj, + Permission, PluginConfiguration, Ref, RefTo, RelatedDocument, + Role, Space, + SpaceType, + SpaceTypeDescriptor, Timestamp, Type, TypeAny, + TypedSpace, UserStatus } from './classes' import { CollaborativeDoc } from './collaboration' @@ -93,6 +98,11 @@ export default plugin(coreId, { TxUpdateDoc: '' as Ref>>, TxRemoveDoc: '' as Ref>>, Space: '' as Ref>, + TypedSpace: '' as Ref>, + SpaceTypeDescriptor: '' as Ref>, + SpaceType: '' as Ref>, + Role: '' as Ref>, + Permission: '' as Ref>, Account: '' as Ref>, Type: '' as Ref>>, TypeString: '' as Ref>>, @@ -157,6 +167,8 @@ export default plugin(coreId, { string: { Id: '' as IntlString, Space: '' as IntlString, + TypedSpace: '' as IntlString, + SpaceType: '' as IntlString, Modified: '' as IntlString, ModifiedDate: '' as IntlString, ModifiedBy: '' as IntlString, @@ -180,6 +192,11 @@ export default plugin(coreId, { Name: '' as IntlString, Enum: '' as IntlString, Description: '' as IntlString, + ShortDescription: '' as IntlString, + Descriptor: '' as IntlString, + TargetClass: '' as IntlString, + Role: '' as IntlString, + Roles: '' as IntlString, Hyperlink: '' as IntlString, Private: '' as IntlString, Object: '' as IntlString, @@ -189,6 +206,16 @@ export default plugin(coreId, { Status: '' as IntlString, Account: '' as IntlString, StatusCategory: '' as IntlString, - Rank: '' as IntlString + Rank: '' as IntlString, + Permission: '' as IntlString, + CreateObject: '' as IntlString, + UpdateObject: '' as IntlString, + DeleteObject: '' as IntlString, + DeleteObjectDescription: '' as IntlString + }, + permission: { + CreateObject: '' as Ref, + UpdateObject: '' as Ref, + DeleteObject: '' as Ref } }) diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index fe9773ad7a..c73267e3a2 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -14,11 +14,26 @@ // import { deepEqual } from 'fast-equals' -import { Account, AnyAttribute, Class, Doc, DocData, DocIndexState, IndexKind, Obj, Ref, Space } from './classes' +import { + Account, + AnyAttribute, + Class, + Doc, + DocData, + DocIndexState, + IndexKind, + Obj, + Permission, + Ref, + Role, + Space, + TypedSpace +} from './classes' import core from './component' import { Hierarchy } from './hierarchy' import { isPredicate } from './predicate' import { DocumentQuery, FindResult } from './storage' +import { TxOperations } from './operations' function toHex (value: number, chars: number): string { const result = value.toString(16) @@ -542,3 +557,30 @@ export const isEnum = (token: any): token is T[keyof T] => { return typeof token === 'string' && Object.values(e as Record).includes(token) } + +export async function checkPermission ( + client: TxOperations, + _id: Ref, + _space: Ref +): Promise { + const space = await client.findOne(core.class.TypedSpace, { _id: _space }) + const type = await client + .getModel() + .findOne(core.class.SpaceType, { _id: space?.type }, { lookup: { _id: { roles: core.class.Role } } }) + const mixin = type?.targetClass + if (space === undefined || type === undefined || mixin === undefined) { + return false + } + + const me = getCurrentAccount() + const asMixin = client.getHierarchy().as(space, mixin) + const myRoles = type.$lookup?.roles?.filter((role) => (asMixin as any)[role._id]?.includes(me._id)) as Role[] + + if (myRoles === undefined) { + return false + } + + const myPermissions = new Set(myRoles.flatMap((role) => role.permissions)) + + return myPermissions.has(_id) +} diff --git a/plugins/attachment-resources/src/components/AttachmentPresenter.svelte b/plugins/attachment-resources/src/components/AttachmentPresenter.svelte index 32cb3b2949..dcc92d0eec 100644 --- a/plugins/attachment-resources/src/components/AttachmentPresenter.svelte +++ b/plugins/attachment-resources/src/components/AttachmentPresenter.svelte @@ -19,6 +19,8 @@ import { showPopup, closeTooltip, Label, getIconSize2x, Loading } from '@hcengineering/ui' import presentation, { PDFViewer, getFileUrl } from '@hcengineering/presentation' import filesize from 'filesize' + import core from '@hcengineering/core' + import { permissionsStore } from '@hcengineering/view-resources' import AttachmentName from './AttachmentName.svelte' @@ -35,6 +37,13 @@ const trimFilename = (fname: string): string => fname.length > maxLenght ? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2) : fname + $: canRemove = + removable && + value !== undefined && + value.readonly !== true && + ($permissionsStore.whitelist.has(value.space) || + $permissionsStore.ps[value.space]?.has(core.permission.DeleteObject)) + function iconLabel (name: string): string { const parts = `${name}`.split('.') const ext = parts[parts.length - 1] @@ -156,7 +165,7 @@ >