EZQMS-729: Restrict spaces operations (#5500)

This commit is contained in:
Alexey Zinoviev 2024-05-04 17:49:24 +04:00 committed by GitHub
parent ad59463057
commit 4bc1534ee0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 873 additions and 114 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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<void> {
// Move statuses to model:
@ -75,6 +77,44 @@ async function migrateStatusesToModel (client: MigrationClient): Promise<void> {
}
}
async function migrateAllSpaceToTyped (client: MigrationClient): Promise<void> {
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<void> {
const targetClasses = client.hierarchy.getDescendants(core.class.Space)
const targetSpaces = await client.find<Space>(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<void> {
// 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
)

View File

@ -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
)
}

View File

@ -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<Ref<Account>>
@Prop(ArrOf(TypeRef(core.class.Account)), core.string.Owners)
owners?: Ref<Account>[]
}
@Model(core.class.TypedSpace, core.class.Space)
@ -86,6 +91,7 @@ export class TSpaceTypeDescriptor extends TDoc implements SpaceTypeDescriptor {
icon!: Asset
baseClass!: Ref<Class<Space>>
availablePermissions!: Ref<Permission>[]
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<Role>]: Ref<Account>[]
}
@Model(core.class.Account, core.class.Doc, DOMAIN_MODEL)
@UX(core.string.Account)
export class TAccount extends TDoc implements Account {

View File

@ -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
)
}
}

View File

@ -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'],

View File

@ -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'],

View File

@ -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<void>
)
}
async function migrateDefaultProjectOwners (client: MigrationClient): Promise<void> {
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<void> {
await tryMigrate(client, leadId, [
@ -167,6 +187,10 @@ export const leadOperation: MigrateOperation = {
func: async (client) => {
await migrateDefaultTypeMixins(client)
}
},
{
state: 'migrateDefaultProjectOwners',
func: migrateDefaultProjectOwners
}
])
},

View File

@ -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<Funnel>
},
viewlet: {
TableCustomer: '' as Ref<Viewlet>,
TableLead: '' as Ref<Viewlet>,

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

View File

@ -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
}
})
}

View File

@ -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"
}
}

View File

@ -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,

View File

@ -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

View File

@ -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: {

View File

@ -48,6 +48,7 @@ import {
taskId,
type ProjectStatus
} from '@hcengineering/task'
import task from './plugin'
import { DOMAIN_TASK } from '.'

View File

@ -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
},

View File

@ -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]
},

View File

@ -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<void> {
const current = await tx.findOne(tracker.class.Project, {
@ -332,6 +335,24 @@ async function migrateDefaultTypeMixins (client: MigrationClient): Promise<void>
)
}
async function migrateDefaultProjectOwners (client: MigrationClient): Promise<void> {
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<void> {
await tryMigrate(client, trackerId, [
@ -358,6 +379,10 @@ export const trackerOperation: MigrateOperation = {
{
state: 'migrateDefaultTypeMixins',
func: migrateDefaultTypeMixins
},
{
state: 'migrateDefaultProjectOwners',
func: migrateDefaultProjectOwners
}
])
},

View File

@ -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]
},

View File

@ -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<boolean>>
CanDeleteObject: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanEditSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanArchiveSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanDeleteSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
},
pipeline: {
PresentationMiddleware: '' as Ref<PresentationMiddlewareFactory>,

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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": "Дает пользователям разрешение архивировать пространство"
}
}

View File

@ -366,6 +366,7 @@ export interface Space extends Doc {
private: boolean
members: Arr<Ref<Account>>
archived: boolean
owners?: Ref<Account>[]
}
/**
@ -388,6 +389,7 @@ export interface SpaceTypeDescriptor extends Doc {
icon: Asset
baseClass: Ref<Class<Space>> // Child class of Space for which the space type can be defined
availablePermissions: Ref<Permission>[]
system?: boolean
}
/**

View File

@ -150,13 +150,14 @@ export default plugin(coreId, {
mixin: {
FullTextSearchContext: '' as Ref<Mixin<FullTextSearchContext>>,
ConfigurationElement: '' as Ref<Mixin<ConfigurationElement>>,
IndexConfiguration: '' as Ref<Mixin<IndexingConfiguration<Doc>>>
IndexConfiguration: '' as Ref<Mixin<IndexingConfiguration<Doc>>>,
SpacesTypeData: '' as Ref<Mixin<Space>>
},
space: {
Tx: '' as Ref<Space>,
DerivedTx: '' as Ref<Space>,
Model: '' as Ref<Space>,
Space: '' as Ref<Space>,
Space: '' as Ref<TypedSpace>,
Configuration: '' as Ref<Space>
},
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<SpaceTypeDescriptor>
},
spaceType: {
SpacesType: '' as Ref<SpaceType>
},
permission: {
CreateObject: '' as Ref<Permission>,
UpdateObject: '' as Ref<Permission>,
DeleteObject: '' as Ref<Permission>,
ForbidDeleteObject: '' as Ref<Permission>
ForbidDeleteObject: '' as Ref<Permission>,
UpdateSpace: '' as Ref<Permission>,
ArchiveSpace: '' as Ref<Permission>
},
role: {
Admin: '' as Ref<Role>
}
})

View File

@ -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<Attribute<PropertyType>>
}
/**
* @public
*/
export function getRoleAttributeBaseProps (data: AttachedData<Role>, roleId: Ref<Role>): RoleAttributeBaseProps {
const name = data.name.trim()
const label = getEmbeddedLabel(`Role: ${name}`)
const id = `role-${roleId}` as Ref<Attribute<PropertyType>>
return { label, id }
}
/**
* @public
*/

View File

@ -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 @@
</div>
</Button>
</div>
<Button
width="1.75rem"
{kind}
shape="rectangle-left"
justify="center"
borderStyle="none"
on:click={openDropdown}
{size}
{disabled}
{loading}
>
<div slot="icon">
{#if dropdownIcon}
<Icon icon={dropdownIcon} size="small" />
{/if}
</div>
</Button>
{#if hasDropdown}
<Button
width="1.75rem"
{kind}
shape="rectangle-left"
justify="center"
borderStyle="none"
on:click={openDropdown}
{size}
{disabled}
{loading}
>
<div slot="icon">
{#if dropdownIcon}
<Icon icon={dropdownIcon} size="small" />
{/if}
</div>
</Button>
{/if}
</div>
<style lang="scss">

View File

@ -64,6 +64,8 @@
let isColorSelected = false
let members: Ref<Account>[] =
teamspace?.members !== undefined ? hierarchy.clone(teamspace.members) : [getCurrentAccount()._id]
let owners: Ref<Account>[] =
teamspace?.owners !== undefined ? hierarchy.clone(teamspace.owners) : [getCurrentAccount()._id]
let rolesAssignment: RolesAssignment = {}
$: isNew = teamspace === undefined
@ -115,6 +117,7 @@
description,
private: isPrivate,
members,
owners,
archived: false,
icon,
color
@ -153,6 +156,16 @@
}
}
}
if (teamspaceData.owners?.length !== teamspace?.owners?.length) {
update.owners = teamspaceData.owners
} else {
for (const owner of teamspaceData.owners ?? []) {
if (teamspace.owners?.findIndex((p) => p === owner) === -1) {
update.owners = teamspaceData.owners
break
}
}
}
if (Object.keys(update).length > 0) {
await client.update(teamspace, update)
@ -215,6 +228,13 @@
$: roles = (spaceType?.$lookup?.roles ?? []) as Role[]
function handleOwnersChanged (newOwners: Ref<Account>[]): void {
owners = newOwners
const newMembersSet = new Set([...members, ...newOwners])
members = Array.from(newMembersSet)
}
function handleMembersChanged (newMembers: Ref<Account>[]): void {
// If a member was removed we need to remove it from any roles assignments as well
const newMembersSet = new Set(newMembers)
@ -237,16 +257,21 @@
rolesAssignment[roleId] = newMembers
}
$: canSave =
name.trim().length > 0 &&
!(members.length === 0 && isPrivate) &&
typeId !== undefined &&
spaceType?.targetClass !== undefined &&
owners.length > 0 &&
(!isPrivate || owners.some((o) => members.includes(o)))
</script>
<Card
label={isNew ? documentRes.string.NewTeamspace : documentRes.string.EditTeamspace}
okLabel={isNew ? presentation.string.Create : presentation.string.Save}
okAction={handleSave}
canSave={name.trim().length > 0 &&
!(members.length === 0 && isPrivate) &&
typeId !== undefined &&
spaceType?.targetClass !== undefined}
{canSave}
accentHeader
width={'medium'}
gap={'gapV-6'}
@ -324,6 +349,19 @@
/>
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={core.string.Owners} />
</div>
<AccountArrayEditor
value={owners}
label={core.string.Owners}
onChange={handleOwnersChanged}
kind={'regular'}
size={'large'}
/>
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header withDesciption">
<Label label={presentation.string.MakePrivate} />

View File

@ -51,6 +51,7 @@
let members: Ref<Account>[] =
funnel?.members !== undefined ? hierarchy.clone(funnel.members) : [getCurrentAccount()._id]
let owners: Ref<Account>[] = funnel?.owners !== undefined ? hierarchy.clone(funnel.owners) : [getCurrentAccount()._id]
$: void loadSpaceType(typeId)
async function loadSpaceType (id: typeof typeId): Promise<void> {
@ -99,6 +100,7 @@
private: isPrivate,
archived: false,
members,
owners,
type: typeId
})
@ -110,7 +112,7 @@
if (isNew) {
await createFunnel()
} else if (funnel !== undefined && spaceType?.targetClass !== undefined) {
await client.diffUpdate<Funnel>(funnel, { name, description, members, private: isPrivate }, Date.now())
await client.diffUpdate<Funnel>(funnel, { name, description, members, owners, private: isPrivate }, Date.now())
if (!deepEqual(rolesAssignment, getRolesAssignment())) {
await client.updateMixin(
@ -124,6 +126,13 @@
}
}
function handleOwnersChanged (newOwners: Ref<Account>[]): void {
owners = newOwners
const newMembersSet = new Set([...members, ...newOwners])
members = Array.from(newMembersSet)
}
function handleMembersChanged (newMembers: Ref<Account>[]): void {
// If a member was removed we need to remove it from any roles assignments as well
const newMembersSet = new Set(newMembers)
@ -146,13 +155,19 @@
rolesAssignment[roleId] = newMembers
}
$: canSave =
name.trim().length > 0 &&
!(members.length === 0 && isPrivate) &&
owners.length > 0 &&
(!isPrivate || owners.some((o) => members.includes(o)))
</script>
<SpaceCreateCard
label={leadRes.string.CreateFunnel}
okAction={save}
okLabel={!isNew ? ui.string.Save : undefined}
canSave={name.trim().length > 0}
{canSave}
on:close={() => {
dispatch('close')
}}
@ -178,6 +193,19 @@
}}
/>
</Grid>
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={core.string.Owners} />
</div>
<AccountArrayEditor
value={owners}
label={core.string.Owners}
onChange={handleOwnersChanged}
kind={'regular'}
size={'large'}
/>
</div>
<div class="antiGrid-row mt-4">
<div class="antiGrid-row__header">
<Label label={leadRes.string.Members} />

View File

@ -92,6 +92,9 @@ const lead = plugin(leadId, {
},
template: {
DefaultFunnel: '' as Ref<ProjectType>
},
space: {
DefaultFunnel: '' as Ref<Funnel>
}
})

View File

@ -18,11 +18,11 @@
import { AccountArrayEditor, UserBox } from '@hcengineering/contact-resources'
import core, {
Account,
Class,
Data,
fillDefaults,
FindResult,
generateId,
getCurrentAccount,
Ref,
Role,
RolesAssignment,
@ -217,6 +217,7 @@
number: (incResult as any).object.sequence,
company,
members: [],
owners: [getCurrentAccount()._id],
type: typeId
},
objectId

View File

@ -137,7 +137,11 @@
<svelte:fragment slot="attributes" let:direction={dir}>
{#if dir === 'column'}
<DocAttributeBar {object} {mixins} ignoreKeys={['name', 'fullDescription', 'private', 'archived', 'type']} />
<DocAttributeBar
{object}
{mixins}
ignoreKeys={['name', 'fullDescription', 'private', 'archived', 'type', 'owners']}
/>
{/if}
</svelte:fragment>

View File

@ -61,4 +61,7 @@
<path d="M24 21C24 20.4477 24.4477 20 25 20C25.5523 20 26 20.4477 26 21V24H29C29.5523 24 30 24.4477 30 25C30 25.5523 29.5523 26 29 26H26V29C26 29.5523 25.5523 30 25 30C24.4477 30 24 29.5523 24 29V26H21C20.4477 26 20 25.5523 20 25C20 24.4477 20.4477 24 21 24H24V21Z" />
<path d="M13 20C9.13401 20 6 23.134 6 27V29C6 29.5523 6.44772 30 7 30C7.55228 30 8 29.5523 8 29V27C8 24.2386 10.2386 22 13 22H17C17.5523 22 18 21.5523 18 21C18 20.4477 17.5523 20 17 20H13Z" />
</symbol>
<symbol id="views" viewBox="0 0 16 16">
<path d="M12.6541 10.7952L14.7544 11.6213C14.8576 11.6618 14.9394 11.7434 14.9801 11.8466C15.0511 12.0264 14.9828 12.2268 14.827 12.3284L14.755 12.3656L8.35645 14.8924C8.15935 14.9703 7.94372 14.9831 7.74052 14.9309L7.62035 14.8918L1.25259 12.3653C1.1499 12.3246 1.06864 12.2432 1.02806 12.1404C0.957068 11.9607 1.02536 11.7603 1.1812 11.6587L1.25319 11.6215L3.34307 10.7962L7.06917 12.2751C7.65895 12.5091 8.31525 12.5097 8.9054 12.2766L12.6541 10.7952ZM12.6541 6.77688L14.7544 7.60289C14.8576 7.64346 14.9394 7.72508 14.9801 7.82824C15.0511 8.00803 14.9828 8.20839 14.827 8.31004L14.755 8.3472L10.6001 9.98825L9.619 10.375L8.35645 10.8741L8.317 10.886L8.23566 10.9132C8.20301 10.9215 8.17004 10.9282 8.13688 10.9331C8.12585 10.9346 8.11547 10.936 8.10507 10.9372C8.02541 10.9468 7.94422 10.9464 7.86397 10.9363L7.74052 10.9126L7.62035 10.8735L6.391 10.385L5.38907 9.98825L1.25259 8.34697C1.1499 8.30623 1.06864 8.22483 1.02806 8.12208C0.957068 7.94229 1.02536 7.74192 1.1812 7.64029L1.25319 7.60312L3.34307 6.77788L7.06917 8.25677C7.65895 8.49078 8.31525 8.4913 8.9054 8.25824L12.6541 6.77688ZM7.62186 1.06989C7.85734 0.976906 8.11932 0.976697 8.35494 1.06931L14.7544 3.58452C14.8576 3.62509 14.9394 3.70671 14.9801 3.80987C15.0612 4.01534 14.9605 4.24769 14.755 4.32884L10.6001 5.96988L8.35565 6.856L8.27468 6.88396C8.25405 6.8901 8.23326 6.89557 8.21236 6.90036C8.09824 6.92674 7.98013 6.93258 7.86397 6.91788L7.74052 6.89419L7.62035 6.8551L1.25259 4.3286C1.1499 4.28786 1.06864 4.20646 1.02806 4.10371C0.946925 3.89823 1.04772 3.66589 1.25319 3.58475L7.62186 1.06989Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,6 +1,7 @@
{
"string": {
"Setting": "Setting",
"Setting": "Setting",
"Spaces": "Spaces",
"Integrations": "Integrations",
"Support": "Support",
"Privacy": "Privacy",
@ -109,6 +110,7 @@
"CountSpaces": "{count, plural, =0 {No spaces} =1 {# space} other {# spaces}}",
"Roles": "Roles",
"RoleName": "Role name",
"Permissions": "Permissions"
"Permissions": "Permissions",
"Assignees": "Assignees"
}
}

View File

@ -1,6 +1,7 @@
{
"string": {
"Setting": "Configuración",
"Spaces": "Espacios",
"Integrations": "Integraciones",
"Support": "Soporte",
"Privacy": "Privacidad",
@ -100,6 +101,7 @@
"Automations": "Automatizaciones",
"Collections": "Colecciones",
"ClassColon": "Clase:",
"Branding": "Marca"
"Branding": "Marca",
"Assignees": "Atribuídos"
}
}

View File

@ -1,6 +1,7 @@
{
"string": {
"Setting": "Configuração",
"Spaces": "Espaços",
"Integrations": "Integrações",
"Support": "Suporte",
"Privacy": "Privacidade",
@ -100,6 +101,7 @@
"Automations": "Automações",
"Collections": "Coleções",
"ClassColon": "Classe:",
"Branding": "Marca"
"Branding": "Marca",
"Assignees": ""
}
}

View File

@ -1,6 +1,7 @@
{
"string": {
"Setting": "Настройки",
"Spaces": "Пространства",
"Integrations": "Интеграции",
"Support": "Поддержка",
"Privacy": "Конфиденциальность",
@ -110,6 +111,7 @@
"CountSpaces": "{count, plural, =0 {Нет пространств} =1 {# пространство} =2 {# пространства} =3 {# пространства} =4 {# пространства} other {# пространств}}",
"Roles": "Роли",
"RoleName": "Название роли",
"Permissions": "Разрешения"
"Permissions": "Разрешения",
"Assignees": "Назначенные"
}
}

View File

@ -31,5 +31,6 @@ loadMetadata(setting.icon, {
Clazz: `${icons}#clazz`,
Enums: `${icons}#enums`,
InviteSettings: `${icons}#inviteSettings`,
InviteWorkspace: `${icons}#inviteWorkspace`
InviteWorkspace: `${icons}#inviteWorkspace`,
Views: `${icons}#views`
})

View File

@ -0,0 +1,111 @@
<!--
// 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.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Header, Breadcrumb, Label } from '@hcengineering/ui'
import core, { Account, Ref, Role, RolesAssignment, SpaceType, TypedSpace, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { AccountArrayEditor } from '@hcengineering/contact-resources'
import setting from '../plugin'
export let visibleNav: boolean = true
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let space: TypedSpace
let spaceType: WithLookup<SpaceType>
const spaceQuery = createQuery()
spaceQuery.query(
core.class.TypedSpace,
{
_id: core.space.Space
},
(res) => {
space = res[0]
}
)
const typeQuery = createQuery()
$: if (space?.type !== undefined) {
typeQuery.query(
core.class.SpaceType,
{
_id: core.spaceType.SpacesType
},
(res) => {
spaceType = res[0]
},
{
lookup: {
_id: { roles: core.class.Role }
}
}
)
}
$: roles = (spaceType?.$lookup?.roles ?? []) as Role[]
let rolesAssignment: RolesAssignment = {}
$: {
if (space !== undefined && spaceType?.targetClass !== undefined) {
const asMixin = hierarchy.as(space, spaceType?.targetClass)
rolesAssignment = roles.reduce<RolesAssignment>((prev, { _id }) => {
prev[_id] = (asMixin as any)[_id] ?? []
return prev
}, {})
}
}
async function handleRoleAssignmentChanged (roleId: Ref<Role>, newMembers: Ref<Account>[]): Promise<void> {
await client.updateMixin(space._id, space._class, core.space.Space, spaceType.targetClass, {
[roleId]: newMembers
})
}
</script>
<div class="hulyComponent">
<Header minimize={!visibleNav} on:resize={(event) => dispatch('change', event.detail)}>
<Breadcrumb icon={setting.icon.Views} label={setting.string.Spaces} size="large" isCurrent />
</Header>
<div class="hulyComponent-content__column content">
{#each roles as role}
<div class="antiGrid-row">
<div class="antiGrid-row__header">
{role.name}
</div>
<AccountArrayEditor
value={rolesAssignment?.[role._id] ?? []}
label={setting.string.Assignees}
onChange={(refs) => {
handleRoleAssignmentChanged(role._id, refs)
}}
kind="regular"
size="large"
/>
</div>
{/each}
</div>
</div>
<style lang="scss">
.content {
margin: 2rem 3.25rem;
}
</style>

View File

@ -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<any>))
descriptor = descriptors[0]
@ -101,6 +101,7 @@
<ObjectBox
_class={core.class.SpaceTypeDescriptor}
value={descriptor?._id}
docQuery={{ system: { $ne: true } }}
on:change={handleDescriptorSelected}
kind="regular"
size="small"

View File

@ -44,6 +44,7 @@ import WorkspaceSetting from './components/WorkspaceSetting.svelte'
import WorkspaceSettings from './components/WorkspaceSettings.svelte'
import InviteSetting from './components/InviteSetting.svelte'
import Configure from './components/Configure.svelte'
import Spaces from './components/Spaces.svelte'
import setting from './plugin'
import IntegrationPanel from './components/IntegrationPanel.svelte'
import { getOwnerFirstName, getOwnerLastName, getOwnerPosition, getValue, filterDescendants } from './utils'
@ -93,6 +94,7 @@ export default async (): Promise<Resources> => ({
},
component: {
Settings,
Spaces,
Profile,
Password,
WorkspaceSetting,

View File

@ -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
}
})

View File

@ -128,7 +128,8 @@ export default plugin(settingId, {
Owners: '' as Ref<Doc>,
InviteSettings: '' as Ref<Doc>,
WorkspaceSetting: '' as Ref<Doc>,
ManageSpaces: '' as Ref<Doc>
ManageSpaces: '' as Ref<Doc>,
Spaces: '' as Ref<Doc>
},
mixin: {
Editable: '' as Ref<Mixin<Editable>>,
@ -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<TemplateFieldCategory>

View File

@ -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<Role>, roleId: Ref<Role>): 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<Attribute<PropertyType>>
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 (

View File

@ -69,6 +69,8 @@
let defaultAssignee: Ref<Employee> | null | undefined = project?.defaultAssignee ?? null
let members: Ref<Account>[] =
project?.members !== undefined ? hierarchy.clone(project.members) : [getCurrentAccount()._id]
let owners: Ref<Account>[] =
project?.owners !== undefined ? hierarchy.clone(project.owners) : [getCurrentAccount()._id]
let projectsIdentifiers = new Set<string>()
let isSaving = false
let defaultStatus: Ref<IssueStatus> | 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<Account>[]): void {
owners = newOwners
const newMembersSet = new Set([...members, ...newOwners])
members = Array.from(newMembersSet)
}
function handleMembersChanged (newMembers: Ref<Account>[]): 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)))
</script>
<Card
label={isNew ? tracker.string.NewProject : tracker.string.EditProject}
okLabel={isNew ? presentation.string.Create : presentation.string.Save}
okAction={handleSave}
canSave={name.trim().length > 0 &&
identifier.trim().length > 0 &&
!projectsIdentifiers.has(identifier.toUpperCase()) &&
!(members.length === 0 && isPrivate)}
{canSave}
accentHeader
width={'medium'}
gap={'gapV-6'}
@ -409,14 +434,6 @@
/>
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header withDesciption">
<Label label={presentation.string.MakePrivate} />
<span><Label label={presentation.string.MakePrivateDescription} /></span>
</div>
<Toggle bind:on={isPrivate} disabled={!isPrivate && members.length === 0} />
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={tracker.string.DefaultAssignee} />
@ -448,6 +465,27 @@
{/if}
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={core.string.Owners} />
</div>
<AccountArrayEditor
value={owners}
label={core.string.Owners}
onChange={handleOwnersChanged}
kind={'regular'}
size={'large'}
/>
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header withDesciption">
<Label label={presentation.string.MakePrivate} />
<span><Label label={presentation.string.MakePrivateDescription} /></span>
</div>
<Toggle bind:on={isPrivate} disabled={!isPrivate && members.length === 0} />
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={tracker.string.Members} />

View File

@ -120,7 +120,7 @@ import {
import { IndexedDocumentPreview } from '@hcengineering/presentation'
import { AggregationMiddleware, AnalyticsMiddleware } from './middleware'
import { showEmptyGroups } from './viewOptions'
import { canDeleteObject } from './visibilityTester'
import { canArchiveSpace, canDeleteObject, canDeleteSpace, canEditSpace } from './visibilityTester'
export { getActions, getContextActions, invokeAction, showMenu } from './actions'
export { default as ActionButton } from './components/ActionButton.svelte'
export { default as ActionHandler } from './components/ActionHandler.svelte'
@ -303,6 +303,9 @@ export default async (): Promise<Resources> => ({
CreateDocMiddleware: AggregationMiddleware.create,
// eslint-disable-next-line @typescript-eslint/unbound-method
AnalyticsMiddleware: AnalyticsMiddleware.create,
CanDeleteObject: canDeleteObject
CanDeleteObject: canDeleteObject,
CanEditSpace: canEditSpace,
CanArchiveSpace: canArchiveSpace,
CanDeleteSpace: canDeleteSpace
}
})

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import core, { checkPermission, type Space, type Doc, type TypedSpace } from '@hcengineering/core'
import core, { checkPermission, type Space, type Doc, type TypedSpace, getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
function isTypedSpace (space: Space): space is TypedSpace {
@ -40,3 +40,71 @@ export async function canDeleteObject (doc?: Doc | Doc[]): Promise<boolean> {
)
).some((r) => r)
}
export async function canEditSpace (doc?: Doc | Doc[]): Promise<boolean> {
if (doc === undefined || Array.isArray(doc)) {
return false
}
const space = doc as Space
if (space.owners?.includes(getCurrentAccount()._id) ?? false) {
return true
}
const client = getClient()
if (await checkPermission(client, core.permission.UpdateObject, core.space.Space)) {
return true
}
if (isTypedSpace(space) && (await checkPermission(client, core.permission.UpdateSpace, space._id))) {
return true
}
return false
}
export async function canArchiveSpace (doc?: Doc | Doc[]): Promise<boolean> {
if (doc === undefined || Array.isArray(doc)) {
return false
}
const space = doc as Space
if (space.owners?.includes(getCurrentAccount()._id) ?? false) {
return true
}
const client = getClient()
if (await checkPermission(client, core.permission.DeleteObject, core.space.Space)) {
return true
}
if (isTypedSpace(space) && (await checkPermission(client, core.permission.ArchiveSpace, space._id))) {
return true
}
return false
}
export async function canDeleteSpace (doc?: Doc | Doc[]): Promise<boolean> {
if (doc === undefined || Array.isArray(doc)) {
return false
}
const space = doc as Space
if (space.owners?.includes(getCurrentAccount()._id) ?? false) {
return true
}
const client = getClient()
if (await checkPermission(client, core.permission.DeleteObject, core.space.Space)) {
return true
}
return false
}

View File

@ -13,8 +13,9 @@
// limitations under the License.
//
import { concatLink, Doc } from '@hcengineering/core'
import { Lead, leadId } from '@hcengineering/lead'
import { PersonAccount } from '@hcengineering/contact'
import core, { concatLink, Doc, Tx, TxUpdateDoc } from '@hcengineering/core'
import lead, { Lead, leadId } from '@hcengineering/lead'
import { getMetadata } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import view from '@hcengineering/view'
@ -39,10 +40,44 @@ export async function leadTextPresenter (doc: Doc): Promise<string> {
return `LEAD-${lead.number}`
}
/**
* @public
*/
export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const targetFunnel = (
await control.findAll(lead.class.Funnel, {
_id: lead.space.DefaultFunnel
})
)[0]
if (targetFunnel === undefined) {
return []
}
const res: Tx[] = []
const actualTx = tx as TxUpdateDoc<PersonAccount>
if (
targetFunnel.owners === undefined ||
targetFunnel.owners.length === 0 ||
targetFunnel.owners[0] === core.account.System
) {
const updTx = control.txFactory.createTxUpdateDoc(lead.class.Funnel, targetFunnel.space, targetFunnel._id, {
owners: [actualTx.objectId]
})
res.push(updTx)
}
return res
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
function: {
LeadHTMLPresenter: leadHTMLPresenter,
LeadTextPresenter: leadTextPresenter
},
trigger: {
OnWorkspaceOwnerAdded
}
})

View File

@ -15,6 +15,7 @@
import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { TriggerFunc } from '@hcengineering/server-core'
import { Presenter } from '@hcengineering/server-notification'
/**
@ -29,5 +30,8 @@ export default plugin(serverLeadId, {
function: {
LeadHTMLPresenter: '' as Resource<Presenter>,
LeadTextPresenter: '' as Resource<Presenter>
},
trigger: {
OnWorkspaceOwnerAdded: '' as Resource<TriggerFunc>
}
})

View File

@ -179,6 +179,37 @@ export async function OnComponentRemove (tx: Tx, control: TriggerControl): Promi
return res
}
/**
* @public
*/
export async function OnWorkspaceOwnerAdded (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const targetProject = (
await control.findAll(tracker.class.Project, {
_id: tracker.project.DefaultProject
})
)[0]
if (targetProject === undefined) {
return []
}
const res: Tx[] = []
const actualTx = tx as TxUpdateDoc<PersonAccount>
if (
targetProject.owners === undefined ||
targetProject.owners.length === 0 ||
targetProject.owners[0] === core.account.System
) {
const updTx = control.txFactory.createTxUpdateDoc(tracker.class.Project, targetProject.space, targetProject._id, {
owners: [actualTx.objectId]
})
res.push(updTx)
}
return res
}
/**
* @public
*/
@ -245,19 +276,6 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
return []
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
function: {
IssueHTMLPresenter: issueHTMLPresenter,
IssueTextPresenter: issueTextPresenter,
IssueNotificationContentProvider: getIssueNotificationContent
},
trigger: {
OnIssueUpdate,
OnComponentRemove
}
})
async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control: TriggerControl): Promise<Tx[]> {
const parentTx = tx as TxCollectionCUD<Issue, TimeSpendReport>
switch (cud._class) {
@ -465,3 +483,17 @@ function updateIssueParentEstimations (
)
}
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
function: {
IssueHTMLPresenter: issueHTMLPresenter,
IssueTextPresenter: issueTextPresenter,
IssueNotificationContentProvider: getIssueNotificationContent
},
trigger: {
OnIssueUpdate,
OnComponentRemove,
OnWorkspaceOwnerAdded
}
})

View File

@ -34,6 +34,7 @@ export default plugin(serverTrackerId, {
},
trigger: {
OnIssueUpdate: '' as Resource<TriggerFunc>,
OnComponentRemove: '' as Resource<TriggerFunc>
OnComponentRemove: '' as Resource<TriggerFunc>,
OnWorkspaceOwnerAdded: '' as Resource<TriggerFunc>
}
})

View File

@ -25,10 +25,10 @@ export class TalentsPage extends CommonRecruitingPage {
inputSearchTalent = (): Locator => this.page.locator('div[class*="header"] input')
andreyTalet = (): Locator => this.page.locator('text=P. Andrey')
addApplicationButton = (): Locator => this.page.locator('button[id="appls.add"]')
spaceSelector = (): Locator => this.page.locator('[id="space.selector"]')
searchInput = (): Locator => this.page.locator('[placeholder="Search..."]')
hrInterviewButton = (): Locator =>
readonly addApplicationButton = (): Locator => this.page.locator('button[id="appls.add"]')
readonly spaceSelector = (): Locator => this.page.locator('[id="space.selector"]')
readonly searchInput = (): Locator => this.page.locator('[placeholder="Search..."]')
readonly hrInterviewButton = (): Locator =>
this.page.locator('[id="recruit:string:CreateApplication"] button:has-text("HR Interview")')
createButton = (): Locator => this.page.locator('button:has-text("Create")')