From 4bc1534ee03ef611df27da9a8a5b6f4374bca6bb Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Sat, 4 May 2024 17:49:24 +0400 Subject: [PATCH] EZQMS-729: Restrict spaces operations (#5500) --- models/board/src/index.ts | 6 +- models/core/src/index.ts | 2 + models/core/src/migration.ts | 55 ++++++++- models/core/src/permissions.ts | 46 +++++++- models/core/src/security.ts | 16 ++- models/core/src/spaceType.ts | 81 +++++++++++++ models/document/src/index.ts | 7 +- models/lead/src/index.ts | 1 + models/lead/src/migration.ts | 26 +++- models/lead/src/plugin.ts | 5 +- models/lead/src/spaceType.ts | 6 +- models/recruit/src/spaceType.ts | 2 +- models/server-lead/package.json | 3 +- models/server-lead/src/index.ts | 13 +- models/server-tracker/package.json | 3 +- models/server-tracker/src/index.ts | 12 +- models/setting/src/index.ts | 17 ++- models/task/src/index.ts | 2 + models/task/src/migration.ts | 1 + models/tracker/src/actions.ts | 4 + models/tracker/src/index.ts | 6 +- models/tracker/src/migration.ts | 27 ++++- models/view/src/index.ts | 1 + models/view/src/plugin.ts | 5 +- packages/core/lang/en.json | 13 +- packages/core/lang/es.json | 5 +- packages/core/lang/pt.json | 5 +- packages/core/lang/ru.json | 13 +- packages/core/src/classes.ts | 2 + packages/core/src/component.ts | 31 ++++- packages/core/src/utils.ts | 23 ++++ .../src/components/ButtonWithDropdown.svelte | 39 +++--- .../teamspace/CreateTeamspace.svelte | 46 +++++++- .../src/components/CreateFunnel.svelte | 32 ++++- plugins/lead/src/index.ts | 3 + .../src/components/CreateVacancy.svelte | 3 +- .../src/components/EditVacancy.svelte | 6 +- plugins/setting-assets/assets/icons.svg | 3 + plugins/setting-assets/lang/en.json | 6 +- plugins/setting-assets/lang/es.json | 4 +- plugins/setting-assets/lang/pt.json | 4 +- plugins/setting-assets/lang/ru.json | 4 +- plugins/setting-assets/src/index.ts | 3 +- .../src/components/Spaces.svelte | 111 ++++++++++++++++++ .../spaceTypes/CreateSpaceType.svelte | 3 +- plugins/setting-resources/src/index.ts | 2 + plugins/setting-resources/src/plugin.ts | 6 +- plugins/setting/src/index.ts | 7 +- plugins/setting/src/utils.ts | 15 ++- .../components/projects/CreateProject.svelte | 62 ++++++++-- plugins/view-resources/src/index.ts | 7 +- .../view-resources/src/visibilityTester.ts | 70 ++++++++++- server-plugins/lead-resources/src/index.ts | 39 +++++- server-plugins/lead/src/index.ts | 4 + server-plugins/tracker-resources/src/index.ts | 58 +++++++-- server-plugins/tracker/src/index.ts | 3 +- .../tests/model/recruiting/talents-page.ts | 8 +- 57 files changed, 873 insertions(+), 114 deletions(-) create mode 100644 models/core/src/spaceType.ts create mode 100644 plugins/setting-resources/src/components/Spaces.svelte diff --git a/models/board/src/index.ts b/models/board/src/index.ts index 269bd27243..af26a01438 100644 --- a/models/board/src/index.ts +++ b/models/board/src/index.ts @@ -505,7 +505,11 @@ export function createModel (builder: Builder): void { description: board.string.ManageBoardStatuses, icon: board.icon.Board, baseClass: board.class.Board, - availablePermissions: [core.permission.ForbidDeleteObject], + availablePermissions: [ + core.permission.UpdateSpace, + core.permission.ArchiveSpace, + core.permission.ForbidDeleteObject + ], allowedTaskTypeDescriptors: [board.descriptors.Card] }, board.descriptors.BoardType diff --git a/models/core/src/index.ts b/models/core/src/index.ts index 092d4058e8..63311e9ca8 100644 --- a/models/core/src/index.ts +++ b/models/core/src/index.ts @@ -103,6 +103,7 @@ import { TTxUpdateDoc, TTxWorkspaceEvent } from './tx' +import { defineSpaceType } from './spaceType' export { coreId } from '@hcengineering/core' export * from './core' @@ -328,4 +329,5 @@ export function createModel (builder: Builder): void { }) definePermissions(builder) + defineSpaceType(builder) } diff --git a/models/core/src/migration.ts b/models/core/src/migration.ts index dd0121242b..ba2b4d179e 100644 --- a/models/core/src/migration.ts +++ b/models/core/src/migration.ts @@ -22,7 +22,8 @@ import core, { TxOperations, generateId, DOMAIN_TX, - type TxCreateDoc + type TxCreateDoc, + type Space } from '@hcengineering/core' import { tryMigrate, @@ -31,6 +32,7 @@ import { type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model' +import { DOMAIN_SPACE } from './security' async function migrateStatusesToModel (client: MigrationClient): Promise { // Move statuses to model: @@ -75,6 +77,44 @@ async function migrateStatusesToModel (client: MigrationClient): Promise { } } +async function migrateAllSpaceToTyped (client: MigrationClient): Promise { + await client.update( + DOMAIN_SPACE, + { + _id: core.space.Space, + _class: core.class.Space + }, + { + $set: { + _class: core.class.TypedSpace, + type: core.spaceType.SpacesType + } + } + ) +} + +async function migrateSpacesOwner (client: MigrationClient): Promise { + const targetClasses = client.hierarchy.getDescendants(core.class.Space) + const targetSpaces = await client.find(DOMAIN_SPACE, { + _class: { $in: targetClasses }, + owners: { $exists: false } + }) + + for (const space of targetSpaces) { + await client.update( + DOMAIN_SPACE, + { + _id: space._id + }, + { + $set: { + owners: [space.createdBy] + } + } + ) + } +} + export const coreOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { // We need to delete all documents in doc index state for missing classes @@ -95,6 +135,14 @@ export const coreOperation: MigrateOperation = { { state: 'statuses-to-model', func: migrateStatusesToModel + }, + { + state: 'all-space-to-typed', + func: migrateAllSpaceToTyped + }, + { + state: 'add-spaces-owner', + func: migrateSpacesOwner } ]) }, @@ -110,14 +158,15 @@ export const coreOperation: MigrateOperation = { }) if (spaceSpace === undefined) { await tx.createDoc( - core.class.Space, + core.class.TypedSpace, core.space.Space, { name: 'Space for all spaces', description: 'Spaces', private: false, archived: false, - members: [] + members: [], + type: core.spaceType.SpacesType }, core.space.Space ) diff --git a/models/core/src/permissions.ts b/models/core/src/permissions.ts index 0b16ce9052..6bb0c0e00b 100644 --- a/models/core/src/permissions.ts +++ b/models/core/src/permissions.ts @@ -22,7 +22,8 @@ export function definePermissions (builder: Builder): void { core.class.Permission, core.space.Model, { - label: core.string.CreateObject + label: core.string.CreateObject, + description: core.string.CreateObjectDescription }, core.permission.CreateObject ) @@ -31,7 +32,8 @@ export function definePermissions (builder: Builder): void { core.class.Permission, core.space.Model, { - label: core.string.UpdateObject + label: core.string.UpdateObject, + description: core.string.UpdateObjectDescription }, core.permission.UpdateObject ) @@ -55,4 +57,44 @@ export function definePermissions (builder: Builder): void { }, core.permission.ForbidDeleteObject ) + + builder.createDoc( + core.class.Permission, + core.space.Model, + { + label: core.string.UpdateObject, + description: core.string.UpdateObjectDescription + }, + core.permission.UpdateObject + ) + + builder.createDoc( + core.class.Permission, + core.space.Model, + { + label: core.string.DeleteObject, + description: core.string.DeleteObjectDescription + }, + core.permission.DeleteObject + ) + + builder.createDoc( + core.class.Permission, + core.space.Model, + { + label: core.string.UpdateSpace, + description: core.string.UpdateSpaceDescription + }, + core.permission.UpdateSpace + ) + + builder.createDoc( + core.class.Permission, + core.space.Model, + { + label: core.string.ArchiveSpace, + description: core.string.ArchiveSpaceDescription + }, + core.permission.ArchiveSpace + ) } diff --git a/models/core/src/security.ts b/models/core/src/security.ts index 58a317b2eb..36c170b287 100644 --- a/models/core/src/security.ts +++ b/models/core/src/security.ts @@ -28,13 +28,15 @@ import { type Role, type Class, type Permission, - type CollectionSize + type CollectionSize, + type RolesAssignment } from '@hcengineering/core' import { ArrOf, Collection, Hidden, Index, + Mixin, Model, Prop, TypeBoolean, @@ -42,7 +44,7 @@ import { TypeString, UX } from '@hcengineering/model' -import type { Asset, IntlString } from '@hcengineering/platform' +import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform' import core from './component' import { TDoc, TAttachedDoc } from './core' @@ -70,6 +72,9 @@ export class TSpace extends TDoc implements Space { @Prop(ArrOf(TypeRef(core.class.Account)), core.string.Members) @Hidden() members!: Arr> + + @Prop(ArrOf(TypeRef(core.class.Account)), core.string.Owners) + owners?: Ref[] } @Model(core.class.TypedSpace, core.class.Space) @@ -86,6 +91,7 @@ export class TSpaceTypeDescriptor extends TDoc implements SpaceTypeDescriptor { icon!: Asset baseClass!: Ref> availablePermissions!: Ref[] + system?: boolean } @Model(core.class.SpaceType, core.class.Doc, DOMAIN_MODEL) @@ -141,6 +147,12 @@ export class TPermission extends TDoc implements Permission { icon?: Asset } +@Mixin(core.mixin.SpacesTypeData, core.class.Space) +@UX(getEmbeddedLabel("All spaces' type")) // TODO: add icon? +export class TSpacesTypeData extends TSpace implements RolesAssignment { + [key: Ref]: Ref[] +} + @Model(core.class.Account, core.class.Doc, DOMAIN_MODEL) @UX(core.string.Account) export class TAccount extends TDoc implements Account { diff --git a/models/core/src/spaceType.ts b/models/core/src/spaceType.ts new file mode 100644 index 0000000000..b55938776f --- /dev/null +++ b/models/core/src/spaceType.ts @@ -0,0 +1,81 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { ArrOf, Prop, TypeRef, type Builder } from '@hcengineering/model' +import { type Asset } from '@hcengineering/platform' +import { getRoleAttributeBaseProps } from '@hcengineering/core' + +import { TSpacesTypeData } from './security' +import core from './component' + +const roles = [ + { + _id: core.role.Admin, + name: 'Admin', + permissions: [core.permission.UpdateObject, core.permission.DeleteObject] + } +] + +export function defineSpaceType (builder: Builder): void { + for (const role of roles) { + const { label, id } = getRoleAttributeBaseProps(role, role._id) + const roleAssgtType = ArrOf(TypeRef(core.class.Account)) + + Prop(roleAssgtType, label)(TSpacesTypeData.prototype, id) + } + + builder.createModel(TSpacesTypeData) + + builder.createDoc( + core.class.SpaceTypeDescriptor, + core.space.Model, + { + name: core.string.Spaces, + description: core.string.SpacesDescription, + icon: '' as Asset, // FIXME + baseClass: core.class.Space, + availablePermissions: [core.permission.UpdateObject, core.permission.DeleteObject], + system: true + }, + core.descriptor.SpacesType + ) + + builder.createDoc( + core.class.SpaceType, + core.space.Model, + { + name: "All spaces' space type", + descriptor: core.descriptor.SpacesType, + roles: roles.length, + targetClass: core.mixin.SpacesTypeData + }, + core.spaceType.SpacesType + ) + + for (const role of roles) { + builder.createDoc( + core.class.Role, + core.space.Model, + { + attachedTo: core.spaceType.SpacesType, + attachedToClass: core.class.SpaceType, + collection: 'roles', + name: role.name, + permissions: role.permissions + }, + role._id + ) + } +} diff --git a/models/document/src/index.ts b/models/document/src/index.ts index 0703fccea1..8d3648cad1 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -180,7 +180,11 @@ function defineTeamspace (builder: Builder): void { description: document.string.Description, icon: document.icon.Document, baseClass: document.class.Teamspace, - availablePermissions: [core.permission.ForbidDeleteObject] + availablePermissions: [ + core.permission.UpdateSpace, + core.permission.ArchiveSpace, + core.permission.ForbidDeleteObject + ] }, document.descriptor.TeamspaceType ) @@ -218,6 +222,7 @@ function defineTeamspace (builder: Builder): void { input: 'focus', category: document.category.Document, target: document.class.Teamspace, + visibilityTester: view.function.CanEditSpace, query: {}, context: { mode: ['context', 'browser'], diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index 84dd1ada2d..3f96ea1e12 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -599,6 +599,7 @@ export function createModel (builder: Builder): void { input: 'focus', category: lead.category.Lead, target: lead.class.Funnel, + visibilityTester: view.function.CanEditSpace, override: [view.action.Open], context: { mode: ['context', 'browser'], diff --git a/models/lead/src/migration.ts b/models/lead/src/migration.ts index c11c7b2685..7887118e15 100644 --- a/models/lead/src/migration.ts +++ b/models/lead/src/migration.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { DOMAIN_TX, type Ref, type Status, TxOperations } from '@hcengineering/core' +import { AccountRole, DOMAIN_TX, type Ref, type Status, TxOperations } from '@hcengineering/core' import { type Lead, leadId } from '@hcengineering/lead' import { type ModelLogger, @@ -24,7 +24,9 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import core, { DOMAIN_SPACE } from '@hcengineering/model-core' + import task, { DOMAIN_TASK, createSequence, migrateDefaultStatusesBase } from '@hcengineering/model-task' +import contact from '@hcengineering/model-contact' import lead from './plugin' import { defaultLeadStatuses } from './spaceType' @@ -147,6 +149,24 @@ async function migrateDefaultTypeMixins (client: MigrationClient): Promise ) } +async function migrateDefaultProjectOwners (client: MigrationClient): Promise { + const workspaceOwners = await client.model.findAll(contact.class.PersonAccount, { + role: AccountRole.Owner + }) + + await client.update( + DOMAIN_SPACE, + { + _id: lead.space.DefaultFunnel + }, + { + $set: { + owners: workspaceOwners.map((it) => it._id) + } + } + ) +} + export const leadOperation: MigrateOperation = { async preMigrate (client: MigrationClient, logger: ModelLogger): Promise { await tryMigrate(client, leadId, [ @@ -167,6 +187,10 @@ export const leadOperation: MigrateOperation = { func: async (client) => { await migrateDefaultTypeMixins(client) } + }, + { + state: 'migrateDefaultProjectOwners', + func: migrateDefaultProjectOwners } ]) }, diff --git a/models/lead/src/plugin.ts b/models/lead/src/plugin.ts index 228a238c79..662571c5c6 100644 --- a/models/lead/src/plugin.ts +++ b/models/lead/src/plugin.ts @@ -16,7 +16,7 @@ import { type ChatMessageViewlet } from '@hcengineering/chunter' import type { Doc, Ref, Status } from '@hcengineering/core' -import { type Funnel, leadId } from '@hcengineering/lead' +import { leadId } from '@hcengineering/lead' import lead from '@hcengineering/lead-resources/src/plugin' import { type NotificationGroup, type NotificationType } from '@hcengineering/notification' import type { IntlString } from '@hcengineering/platform' @@ -45,9 +45,6 @@ export default mergeIds(leadId, lead, { Leads: '' as AnyComponent, NewItemsHeader: '' as AnyComponent }, - space: { - DefaultFunnel: '' as Ref - }, viewlet: { TableCustomer: '' as Ref, TableLead: '' as Ref, diff --git a/models/lead/src/spaceType.ts b/models/lead/src/spaceType.ts index a5f0fdc9c9..633e966a9a 100644 --- a/models/lead/src/spaceType.ts +++ b/models/lead/src/spaceType.ts @@ -88,7 +88,11 @@ export function defineSpaceType (builder: Builder): void { description: plugin.string.ManageFunnelStatuses, icon: plugin.icon.LeadApplication, baseClass: plugin.class.Funnel, - availablePermissions: [core.permission.ForbidDeleteObject], + availablePermissions: [ + core.permission.UpdateSpace, + core.permission.ArchiveSpace, + core.permission.ForbidDeleteObject + ], allowedTaskTypeDescriptors: [plugin.descriptors.Lead] }, plugin.descriptors.FunnelType diff --git a/models/recruit/src/spaceType.ts b/models/recruit/src/spaceType.ts index d32330846a..a4df714da7 100644 --- a/models/recruit/src/spaceType.ts +++ b/models/recruit/src/spaceType.ts @@ -82,7 +82,7 @@ export function defineSpaceType (builder: Builder): void { icon: plugin.icon.RecruitApplication, editor: plugin.component.VacancyTemplateEditor, baseClass: plugin.class.Vacancy, - availablePermissions: [core.permission.ForbidDeleteObject], + availablePermissions: [core.permission.ArchiveSpace, core.permission.ForbidDeleteObject], allowedTaskTypeDescriptors: [plugin.descriptors.Application] }, plugin.descriptors.VacancyType diff --git a/models/server-lead/package.json b/models/server-lead/package.json index d8ecc44605..0883d94469 100644 --- a/models/server-lead/package.json +++ b/models/server-lead/package.json @@ -36,6 +36,7 @@ "@hcengineering/lead": "^0.6.0", "@hcengineering/model-lead": "^0.6.0", "@hcengineering/notification": "^0.6.16", - "@hcengineering/server-notification": "^0.6.1" + "@hcengineering/server-notification": "^0.6.1", + "@hcengineering/contact": "^0.6.20" } } diff --git a/models/server-lead/src/index.ts b/models/server-lead/src/index.ts index c227fcd544..5a70fb4c5f 100644 --- a/models/server-lead/src/index.ts +++ b/models/server-lead/src/index.ts @@ -15,11 +15,13 @@ import { type Builder } from '@hcengineering/model' -import core from '@hcengineering/core' +import core, { AccountRole } from '@hcengineering/core' import lead from '@hcengineering/model-lead' import notification from '@hcengineering/notification' +import serverCore from '@hcengineering/server-core' import serverLead from '@hcengineering/server-lead' import serverNotification from '@hcengineering/server-notification' +import contact from '@hcengineering/contact' export { serverLeadId } from '@hcengineering/server-lead' @@ -40,4 +42,13 @@ export function createModel (builder: Builder): void { func: serverNotification.function.IsUserEmployeeInFieldValue } ) + + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverLead.trigger.OnWorkspaceOwnerAdded, + txMatch: { + _class: core.class.TxUpdateDoc, + objectClass: contact.class.PersonAccount, + 'operations.role': AccountRole.Owner + } + }) } diff --git a/models/server-tracker/package.json b/models/server-tracker/package.json index 13e97d2a1d..9fa8461c3e 100644 --- a/models/server-tracker/package.json +++ b/models/server-tracker/package.json @@ -35,6 +35,7 @@ "@hcengineering/notification": "^0.6.16", "@hcengineering/server-notification": "^0.6.1", "@hcengineering/model-tracker": "^0.6.0", - "@hcengineering/server-tracker": "^0.6.0" + "@hcengineering/server-tracker": "^0.6.0", + "@hcengineering/contact": "^0.6.20" } } diff --git a/models/server-tracker/src/index.ts b/models/server-tracker/src/index.ts index 2487ddfdfc..d51ca8d184 100644 --- a/models/server-tracker/src/index.ts +++ b/models/server-tracker/src/index.ts @@ -13,13 +13,14 @@ // limitations under the License. // -import core from '@hcengineering/core' +import core, { AccountRole } from '@hcengineering/core' import { type Builder } from '@hcengineering/model' import tracker from '@hcengineering/model-tracker' import notification from '@hcengineering/notification' import serverCore from '@hcengineering/server-core' import serverNotification from '@hcengineering/server-notification' import serverTracker from '@hcengineering/server-tracker' +import contact from '@hcengineering/contact' export { serverTrackerId } from '@hcengineering/server-tracker' @@ -59,6 +60,15 @@ export function createModel (builder: Builder): void { } }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverTracker.trigger.OnWorkspaceOwnerAdded, + txMatch: { + _class: core.class.TxUpdateDoc, + objectClass: contact.class.PersonAccount, + 'operations.role': AccountRole.Owner + } + }) + builder.mixin( tracker.ids.AssigneeNotification, notification.class.NotificationType, diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index 5d21991d27..bdcb8c98d2 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -214,6 +214,19 @@ export function createModel (builder: Builder): void { }, setting.ids.Owners ) + builder.createDoc( + setting.class.WorkspaceSettingCategory, + core.space.Model, + { + name: 'allSpaces', + label: setting.string.Spaces, + icon: setting.icon.Views, + component: setting.component.Spaces, + order: 1100, + secured: true + }, + setting.ids.Spaces + ) builder.createDoc( setting.class.WorkspaceSettingCategory, core.space.Model, @@ -222,7 +235,7 @@ export function createModel (builder: Builder): void { label: setting.string.Configure, icon: setting.icon.Setting, component: setting.component.Configure, - order: 1001, + order: 1200, secured: true, adminOnly: true }, @@ -236,7 +249,7 @@ export function createModel (builder: Builder): void { label: setting.string.Branding, icon: setting.icon.AccountSettings, component: setting.component.WorkspaceSetting, - order: 1002, + order: 1300, secured: true }, setting.ids.WorkspaceSetting diff --git a/models/task/src/index.ts b/models/task/src/index.ts index 8513575dc8..33814ad193 100644 --- a/models/task/src/index.ts +++ b/models/task/src/index.ts @@ -266,6 +266,7 @@ export const actionTemplates = template({ label: task.string.Archive, message: task.string.ArchiveConfirm }, + visibilityTester: view.function.CanArchiveSpace, input: 'any', category: task.category.Task, query: { @@ -288,6 +289,7 @@ export const actionTemplates = template({ label: task.string.Unarchive, message: task.string.UnarchiveConfirm }, + visibilityTester: view.function.CanArchiveSpace, input: 'any', category: task.category.Task, query: { diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index 255b5c4a51..5b0aea20de 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -48,6 +48,7 @@ import { taskId, type ProjectStatus } from '@hcengineering/task' + import task from './plugin' import { DOMAIN_TASK } from '.' diff --git a/models/tracker/src/actions.ts b/models/tracker/src/actions.ts index 916998206a..7f58b95f6a 100644 --- a/models/tracker/src/actions.ts +++ b/models/tracker/src/actions.ts @@ -97,6 +97,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId: input: 'focus', category: tracker.category.Tracker, target: tracker.class.Project, + visibilityTester: view.function.CanEditSpace, query: {}, context: { mode: ['context', 'browser'], @@ -115,6 +116,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId: input: 'focus', category: tracker.category.Tracker, target: tracker.class.Project, + visibilityTester: view.function.CanArchiveSpace, query: { archived: false }, @@ -135,6 +137,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId: input: 'focus', category: tracker.category.Tracker, target: tracker.class.Project, + visibilityTester: view.function.CanDeleteSpace, query: { archived: true }, @@ -159,6 +162,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId: }, input: 'any', category: tracker.category.Tracker, + visibilityTester: view.function.CanArchiveSpace, query: { archived: true }, diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 8cb2b2ed96..9306639dc4 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -721,7 +721,11 @@ function defineSpaceType (builder: Builder): void { description: tracker.string.ManageWorkflowStatuses, icon: task.icon.Task, baseClass: tracker.class.Project, - availablePermissions: [core.permission.ForbidDeleteObject], + availablePermissions: [ + core.permission.UpdateSpace, + core.permission.ArchiveSpace, + core.permission.ForbidDeleteObject + ], allowedClassic: true, allowedTaskTypeDescriptors: [tracker.descriptors.Issue] }, diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index 740380115b..16a3c69ebf 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -21,7 +21,8 @@ import core, { toIdMap, DOMAIN_TX, type Status, - type Ref + type Ref, + AccountRole } from '@hcengineering/core' import { type ModelLogger, @@ -46,7 +47,9 @@ import { type Project, classicIssueTaskStatuses } from '@hcengineering/tracker' + import tracker from './plugin' +import contact from '@hcengineering/model-contact' async function createDefaultProject (tx: TxOperations): Promise { const current = await tx.findOne(tracker.class.Project, { @@ -332,6 +335,24 @@ async function migrateDefaultTypeMixins (client: MigrationClient): Promise ) } +async function migrateDefaultProjectOwners (client: MigrationClient): Promise { + const workspaceOwners = await client.model.findAll(contact.class.PersonAccount, { + role: AccountRole.Owner + }) + + await client.update( + DOMAIN_SPACE, + { + _id: tracker.project.DefaultProject + }, + { + $set: { + owners: workspaceOwners.map((it) => it._id) + } + } + ) +} + export const trackerOperation: MigrateOperation = { async preMigrate (client: MigrationClient, logger: ModelLogger): Promise { await tryMigrate(client, trackerId, [ @@ -358,6 +379,10 @@ export const trackerOperation: MigrateOperation = { { state: 'migrateDefaultTypeMixins', func: migrateDefaultTypeMixins + }, + { + state: 'migrateDefaultProjectOwners', + func: migrateDefaultProjectOwners } ]) }, diff --git a/models/view/src/index.ts b/models/view/src/index.ts index b0653bd319..cb24265738 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -642,6 +642,7 @@ export function createModel (builder: Builder): void { archived: false }, target: core.class.Space, + visibilityTester: view.function.CanArchiveSpace, context: { mode: ['context', 'browser'], group: 'tools' }, override: [view.action.Delete] }, diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 02d7e6b83c..9c3643ba7c 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -125,7 +125,10 @@ export default mergeIds(viewId, view, { FilterDateNotSpecified: '' as FilterFunction, FilterDateCustom: '' as FilterFunction, ShowEmptyGroups: '' as ViewCategoryAction, - CanDeleteObject: '' as Resource<(doc?: Doc | Doc[]) => Promise> + CanDeleteObject: '' as Resource<(doc?: Doc | Doc[]) => Promise>, + CanEditSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise>, + CanArchiveSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise>, + CanDeleteSpace: '' 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 831ffc36ff..3e4201cb45 100644 --- a/packages/core/lang/en.json +++ b/packages/core/lang/en.json @@ -2,6 +2,8 @@ "string": { "Id": "Id", "Space": "Space", + "Spaces": "Spaces", + "SpacesDescription": "Manage all spaces' space type", "TypedSpace": "Typed space", "SpaceType": "Space type", "Modified": "Modified", @@ -43,12 +45,19 @@ "StatusCategory": "Status category", "Account": "Account", "Rank": "Rank", + "Owners": "Owners", "Permission": "Permission", "CreateObject": "Create object", "UpdateObject": "Update object", "DeleteObject": "Delete object", - "DeleteObjectDescription": "Grants users ability to delete objects in the space", "ForbidDeleteObject": "Forbid delete object", - "ForbidDeleteObjectDescription": "Forbid users deleting objects in the space" + "UpdateSpace": "Update space", + "ArchiveSpace": "Archive space", + "CreateObjectDescription": "Grants users ability to create objects in the space", + "UpdateObjectDescription": "Grants users ability to update objects in the space", + "DeleteObjectDescription": "Grants users ability to delete objects in the space", + "ForbidDeleteObjectDescription": "Forbid users deleting objects in the space", + "UpdateSpaceDescription": "Grants users ability to update the space", + "ArchiveSpaceDescription": "Grants users ability to archive the space" } } diff --git a/packages/core/lang/es.json b/packages/core/lang/es.json index dbc1f0091a..d52668287a 100644 --- a/packages/core/lang/es.json +++ b/packages/core/lang/es.json @@ -2,6 +2,8 @@ "string": { "Id": "Id.", "Space": "Espacio", + "Spaces": "Espacios", + "SpacesDescription": "Gestionar el tipo de espacio de todos los espacios", "Modified": "Modificado", "ModifiedDate": "Fecha de modificación", "ModifiedBy": "Modificado por", @@ -35,6 +37,7 @@ "Status": "Estado", "StatusCategory": "Categoría de estado", "Account": "Cuenta", - "Rank": "Rango" + "Rank": "Rango", + "Owners": "Propietarios" } } diff --git a/packages/core/lang/pt.json b/packages/core/lang/pt.json index f8305f3df9..95245f15bf 100644 --- a/packages/core/lang/pt.json +++ b/packages/core/lang/pt.json @@ -2,6 +2,8 @@ "string": { "Id": "Id", "Space": "Espaço", + "Spaces": "Espaços", + "SpacesDescription": "Gestão do tipo de espaço para todas as espaços", "Modified": "Modificado", "ModifiedDate": "Data de modificação", "ModifiedBy": "Modificado por", @@ -35,6 +37,7 @@ "Status": "Estado", "StatusCategory": "Categoria de estado", "Account": "Conta", - "Rank": "Ranking" + "Rank": "Ranking", + "Owners": "Proprietários" } } diff --git a/packages/core/lang/ru.json b/packages/core/lang/ru.json index 5cee95e1fb..3ff1c54227 100644 --- a/packages/core/lang/ru.json +++ b/packages/core/lang/ru.json @@ -2,6 +2,8 @@ "string": { "Id": "Id", "Space": "Пространство", + "Spaces": "Пространства", + "SpacesDescription": "Управлять типом пространства всех пространств", "TypedSpace": "Типизированное пространство", "SpaceType": "Тип пространства", "Modified": "Изменено", @@ -43,12 +45,19 @@ "StatusCategory": "Категория статуса", "Account": "Аккаунт", "Rank": "Ранг", + "Owners": "Владельцы", "Permission": "Разрешение", "CreateObject": "Создавать объект", "UpdateObject": "Обновлять объект", "DeleteObject": "Удалять объект", - "DeleteObjectDescription": "Дает пользователям разрешение удалять объекты в пространстве", "ForbidDeleteObject": "Запретить удалять объект", - "ForbidDeleteObjectDescription": "Запрещает пользователям удалять объекты в пространстве" + "UpdateSpace": "Обновлять пространство", + "ArchiveSpace": "Архивировать пространство", + "CreateObjectDescription": "Дает пользователям разрешение создавать объекты в пространстве", + "UpdateObjectDescription": "Дает пользователям разрешение обновлять объекты в пространстве", + "DeleteObjectDescription": "Дает пользователям разрешение удалять объекты в пространстве", + "ForbidDeleteObjectDescription": "Запрещает пользователям удалять объекты в пространстве", + "UpdateSpaceDescription": "Дает пользователям разрешение обновлять пространство", + "ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство" } } diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 29c89f1b84..14cfba79ab 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -366,6 +366,7 @@ export interface Space extends Doc { private: boolean members: Arr> archived: boolean + owners?: Ref[] } /** @@ -388,6 +389,7 @@ export interface SpaceTypeDescriptor extends Doc { icon: Asset baseClass: Ref> // Child class of Space for which the space type can be defined availablePermissions: Ref[] + system?: boolean } /** diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index a9d913bb92..6f7f62edc3 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -150,13 +150,14 @@ export default plugin(coreId, { mixin: { FullTextSearchContext: '' as Ref>, ConfigurationElement: '' as Ref>, - IndexConfiguration: '' as Ref>> + IndexConfiguration: '' as Ref>>, + SpacesTypeData: '' as Ref> }, space: { Tx: '' as Ref, DerivedTx: '' as Ref, Model: '' as Ref, - Space: '' as Ref, + Space: '' as Ref, Configuration: '' as Ref }, account: { @@ -174,6 +175,8 @@ export default plugin(coreId, { string: { Id: '' as IntlString, Space: '' as IntlString, + Spaces: '' as IntlString, + SpacesDescription: '' as IntlString, TypedSpace: '' as IntlString, SpaceType: '' as IntlString, Modified: '' as IntlString, @@ -214,18 +217,36 @@ export default plugin(coreId, { Account: '' as IntlString, StatusCategory: '' as IntlString, Rank: '' as IntlString, + Owners: '' as IntlString, Permission: '' as IntlString, CreateObject: '' as IntlString, UpdateObject: '' as IntlString, DeleteObject: '' as IntlString, - DeleteObjectDescription: '' as IntlString, ForbidDeleteObject: '' as IntlString, - ForbidDeleteObjectDescription: '' as IntlString + UpdateSpace: '' as IntlString, + ArchiveSpace: '' as IntlString, + CreateObjectDescription: '' as IntlString, + UpdateObjectDescription: '' as IntlString, + DeleteObjectDescription: '' as IntlString, + ForbidDeleteObjectDescription: '' as IntlString, + UpdateSpaceDescription: '' as IntlString, + ArchiveSpaceDescription: '' as IntlString + }, + descriptor: { + SpacesType: '' as Ref + }, + spaceType: { + SpacesType: '' as Ref }, permission: { CreateObject: '' as Ref, UpdateObject: '' as Ref, DeleteObject: '' as Ref, - ForbidDeleteObject: '' as Ref + ForbidDeleteObject: '' as Ref, + UpdateSpace: '' as Ref, + ArchiveSpace: '' as Ref + }, + role: { + Admin: '' as Ref } }) diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index f8cb3c54c2..59864078c6 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -17,7 +17,9 @@ import { deepEqual } from 'fast-equals' import { Account, AnyAttribute, + AttachedData, AttachedDoc, + Attribute, Class, ClassifierKind, Collection, @@ -33,6 +35,7 @@ import { IndexKind, Obj, Permission, + PropertyType, Ref, Role, Space, @@ -44,6 +47,7 @@ import { TxOperations } from './operations' import { isPredicate } from './predicate' import { DocumentQuery, FindResult } from './storage' import { DOMAIN_TX } from './tx' +import { getEmbeddedLabel, IntlString } from '@hcengineering/platform' function toHex (value: number, chars: number): string { const result = value.toString(16) @@ -601,6 +605,25 @@ export async function checkPermission ( return myPermissions.has(_id) } +/** + * @public + */ +export interface RoleAttributeBaseProps { + label: IntlString + id: Ref> +} + +/** + * @public + */ +export function getRoleAttributeBaseProps (data: AttachedData, roleId: Ref): RoleAttributeBaseProps { + const name = data.name.trim() + const label = getEmbeddedLabel(`Role: ${name}`) + const id = `role-${roleId}` as Ref> + + return { label, id } +} + /** * @public */ diff --git a/packages/ui/src/components/ButtonWithDropdown.svelte b/packages/ui/src/components/ButtonWithDropdown.svelte index 84be715a58..5bcec89ccf 100644 --- a/packages/ui/src/components/ButtonWithDropdown.svelte +++ b/packages/ui/src/components/ButtonWithDropdown.svelte @@ -42,6 +42,7 @@ export let disabled: boolean = false export let loading: boolean = false export let focusIndex: number | undefined = undefined + export let hasDropdown: boolean = true const dispatch = createEventDispatcher() @@ -61,7 +62,7 @@ {size} {kind} disabled={disabled || loading} - shape="rectangle-right" + shape={hasDropdown ? 'rectangle-right' : undefined} {justify} borderStyle="none" on:click @@ -79,23 +80,25 @@ - + {#if hasDropdown} + + {/if} diff --git a/plugins/setting-resources/src/components/spaceTypes/CreateSpaceType.svelte b/plugins/setting-resources/src/components/spaceTypes/CreateSpaceType.svelte index 373d667ee1..12fbdb3969 100644 --- a/plugins/setting-resources/src/components/spaceTypes/CreateSpaceType.svelte +++ b/plugins/setting-resources/src/components/spaceTypes/CreateSpaceType.svelte @@ -54,7 +54,7 @@ const descriptors = client .getModel() - .findAllSync(core.class.SpaceTypeDescriptor, {}) + .findAllSync(core.class.SpaceTypeDescriptor, { system: { $ne: true } }) .filter((descriptor) => hasResource(descriptor._id as any as Resource)) descriptor = descriptors[0] @@ -101,6 +101,7 @@ => ({ }, component: { Settings, + Spaces, Profile, Password, WorkspaceSetting, diff --git a/plugins/setting-resources/src/plugin.ts b/plugins/setting-resources/src/plugin.ts index 31c8e3b9ad..dd68593538 100644 --- a/plugins/setting-resources/src/plugin.ts +++ b/plugins/setting-resources/src/plugin.ts @@ -23,7 +23,8 @@ export default mergeIds(settingId, setting, { EditEnum: '' as AnyComponent, ManageSpaceTypes: '' as AnyComponent, ManageSpaceTypesTools: '' as AnyComponent, - ManageSpaceTypeContent: '' as AnyComponent + ManageSpaceTypeContent: '' as AnyComponent, + Spaces: '' as AnyComponent }, string: { IntegrationDisabled: '' as IntlString, @@ -97,6 +98,7 @@ export default mergeIds(settingId, setting, { Description: '' as IntlString, CountSpaces: '' as IntlString, RoleName: '' as IntlString, - Permissions: '' as IntlString + Permissions: '' as IntlString, + Assignees: '' as IntlString } }) diff --git a/plugins/setting/src/index.ts b/plugins/setting/src/index.ts index 5e21e344d1..9aaa86bbdc 100644 --- a/plugins/setting/src/index.ts +++ b/plugins/setting/src/index.ts @@ -128,7 +128,8 @@ export default plugin(settingId, { Owners: '' as Ref, InviteSettings: '' as Ref, WorkspaceSetting: '' as Ref, - ManageSpaces: '' as Ref + ManageSpaces: '' as Ref, + Spaces: '' as Ref }, mixin: { Editable: '' as Ref>, @@ -169,6 +170,7 @@ export default plugin(settingId, { string: { Settings: '' as IntlString, Setting: '' as IntlString, + Spaces: '' as IntlString, WorkspaceSettings: '' as IntlString, Branding: '' as IntlString, Integrations: '' as IntlString, @@ -219,7 +221,8 @@ export default plugin(settingId, { Clazz: '' as Asset, Enums: '' as Asset, InviteSettings: '' as Asset, - InviteWorkspace: '' as Asset + InviteWorkspace: '' as Asset, + Views: '' as Asset }, templateFieldCategory: { Integration: '' as Ref diff --git a/plugins/setting/src/utils.ts b/plugins/setting/src/utils.ts index 3f31bd099b..24143bdb14 100644 --- a/plugins/setting/src/utils.ts +++ b/plugins/setting/src/utils.ts @@ -25,7 +25,8 @@ import core, { Space, SpaceType, TxOperations, - TypeAny as TypeAnyType + TypeAny as TypeAnyType, + getRoleAttributeBaseProps } from '@hcengineering/core' import { TypeAny } from '@hcengineering/model' import { getEmbeddedLabel, IntlString } from '@hcengineering/platform' @@ -74,12 +75,14 @@ export interface RoleAttributeProps { } export function getRoleAttributeProps (data: AttachedData, roleId: Ref): RoleAttributeProps { - const name = data.name.trim() - const label = getEmbeddedLabel(`Role: ${name}`) - const roleType = TypeAny(setting.component.RoleAssignmentEditor, label, setting.component.RoleAssignmentEditor) - const id = `role-${roleId}` as Ref> + const baseProps = getRoleAttributeBaseProps(data, roleId) + const roleType = TypeAny( + setting.component.RoleAssignmentEditor, + baseProps.label, + setting.component.RoleAssignmentEditor + ) - return { label, roleType, id } + return { ...baseProps, roleType } } export async function createSpaceTypeRole ( diff --git a/plugins/tracker-resources/src/components/projects/CreateProject.svelte b/plugins/tracker-resources/src/components/projects/CreateProject.svelte index 82142fb217..33137d6128 100644 --- a/plugins/tracker-resources/src/components/projects/CreateProject.svelte +++ b/plugins/tracker-resources/src/components/projects/CreateProject.svelte @@ -69,6 +69,8 @@ let defaultAssignee: Ref | null | undefined = project?.defaultAssignee ?? null let members: Ref[] = project?.members !== undefined ? hierarchy.clone(project.members) : [getCurrentAccount()._id] + let owners: Ref[] = + project?.owners !== undefined ? hierarchy.clone(project.owners) : [getCurrentAccount()._id] let projectsIdentifiers = new Set() let isSaving = false let defaultStatus: Ref | undefined = project?.defaultIssueStatus @@ -96,6 +98,7 @@ description, private: isPrivate, members, + owners, archived: false, identifier: identifier.toUpperCase(), sequence: 0, @@ -162,6 +165,16 @@ } } } + if (projectData.owners?.length !== project?.owners?.length) { + update.owners = projectData.owners + } else { + for (const owner of projectData.owners || []) { + if (project.owners?.findIndex((p) => p === owner) === -1) { + update.owners = projectData.owners + break + } + } + } if (Object.keys(update).length > 0) { isSaving = true @@ -275,6 +288,13 @@ rolesQuery.unsubscribe() } + function handleOwnersChanged (newOwners: Ref[]): void { + owners = newOwners + + const newMembersSet = new Set([...members, ...newOwners]) + members = Array.from(newMembersSet) + } + function handleMembersChanged (newMembers: Ref[]): void { // If a member was removed we need to remove it from any roles assignments as well const newMembersSet = new Set(newMembers) @@ -297,16 +317,21 @@ rolesAssignment[roleId] = newMembers } + + $: canSave = + name.trim().length > 0 && + identifier.trim().length > 0 && + !projectsIdentifiers.has(identifier.toUpperCase()) && + !(members.length === 0 && isPrivate) && + owners.length > 0 && + (!isPrivate || owners.some((o) => members.includes(o))) 0 && - identifier.trim().length > 0 && - !projectsIdentifiers.has(identifier.toUpperCase()) && - !(members.length === 0 && isPrivate)} + {canSave} accentHeader width={'medium'} gap={'gapV-6'} @@ -409,14 +434,6 @@ /> -
-
-
- -
-
+
+
+
+ +
+ +
+
+
+ +
+