mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-6001: Roles management (#4994)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
bee1986299
commit
418b4b1bbd
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -57,6 +57,7 @@
|
||||
// "RETRANSLATE_TOKEN": ""
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
"showAsyncStacks": true,
|
||||
"sourceMaps": true,
|
||||
"cwd": "${workspaceRoot}/pods/server",
|
||||
@ -154,7 +155,7 @@
|
||||
"name": "Debug tool upgrade",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": ["src/index.ts", "upgrade"],
|
||||
"args": ["src/__start.ts", "upgrade"],
|
||||
"env": {
|
||||
"SERVER_SECRET": "secret",
|
||||
"MINIO_ACCESS_KEY": "minioadmin",
|
||||
|
@ -18075,6 +18075,7 @@ packages:
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
fast-equals: 2.0.4
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||
@ -18824,6 +18825,7 @@ packages:
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
fast-equals: 2.0.4
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||
|
@ -40,6 +40,7 @@ import { calendarOperation } from '@hcengineering/model-calendar'
|
||||
import { timeOperation } from '@hcengineering/model-time'
|
||||
import { activityOperation } from '@hcengineering/model-activity'
|
||||
import { activityServerOperation } from '@hcengineering/model-server-activity'
|
||||
import { documentOperation } from '@hcengineering/model-document'
|
||||
|
||||
export const migrateOperations: [string, MigrateOperation][] = [
|
||||
['core', coreOperation],
|
||||
@ -66,6 +67,7 @@ export const migrateOperations: [string, MigrateOperation][] = [
|
||||
['inventiry', inventoryOperation],
|
||||
['time', timeOperation],
|
||||
['activityServer', activityServerOperation],
|
||||
['document', documentOperation],
|
||||
// We should call it after activityServer and chunter
|
||||
['notification', notificationOperation]
|
||||
]
|
||||
|
@ -505,6 +505,7 @@ export function createModel (builder: Builder): void {
|
||||
description: board.string.ManageBoardStatuses,
|
||||
icon: board.icon.Board,
|
||||
baseClass: board.class.Board,
|
||||
availablePermissions: [core.permission.DeleteObject],
|
||||
allowedTaskTypeDescriptors: [board.descriptors.Card]
|
||||
},
|
||||
board.descriptors.BoardType
|
||||
|
@ -59,6 +59,7 @@ async function createDefaultProjectType (tx: TxOperations): Promise<Ref<ProjectT
|
||||
descriptor: board.descriptors.BoardType,
|
||||
description: '',
|
||||
tasks: [],
|
||||
roles: 0,
|
||||
classic: false
|
||||
},
|
||||
[
|
||||
|
@ -67,7 +67,7 @@ import {
|
||||
TTypeTimestamp,
|
||||
TVersion
|
||||
} from './core'
|
||||
import { TAccount, TSpace } from './security'
|
||||
import { TAccount, TSpace, TSpaceType, TSpaceTypeDescriptor, TTypedSpace, TRole, TPermission } from './security'
|
||||
import { TStatus, TStatusCategory } from './status'
|
||||
import { TUserStatus } from './transient'
|
||||
import {
|
||||
@ -81,6 +81,7 @@ import {
|
||||
TTxUpdateDoc,
|
||||
TTxWorkspaceEvent
|
||||
} from './tx'
|
||||
import { definePermissions } from './permissions'
|
||||
|
||||
export { coreId } from '@hcengineering/core'
|
||||
export * from './core'
|
||||
@ -108,6 +109,11 @@ export function createModel (builder: Builder): void {
|
||||
TTxApplyIf,
|
||||
TTxWorkspaceEvent,
|
||||
TSpace,
|
||||
TTypedSpace,
|
||||
TSpaceType,
|
||||
TSpaceTypeDescriptor,
|
||||
TRole,
|
||||
TPermission,
|
||||
TAccount,
|
||||
TAttribute,
|
||||
TType,
|
||||
@ -214,4 +220,6 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(core.class.Space, core.class.Class, core.mixin.FullTextSearchContext, {
|
||||
childProcessingAllowed: false
|
||||
})
|
||||
|
||||
definePermissions(builder)
|
||||
}
|
||||
|
48
models/core/src/permissions.ts
Normal file
48
models/core/src/permissions.ts
Normal file
@ -0,0 +1,48 @@
|
||||
//
|
||||
// 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 { type Builder } from '@hcengineering/model'
|
||||
|
||||
import core from './component'
|
||||
|
||||
export function definePermissions (builder: Builder): void {
|
||||
builder.createDoc(
|
||||
core.class.Permission,
|
||||
core.space.Model,
|
||||
{
|
||||
label: core.string.CreateObject
|
||||
},
|
||||
core.permission.CreateObject
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.Permission,
|
||||
core.space.Model,
|
||||
{
|
||||
label: core.string.UpdateObject
|
||||
},
|
||||
core.permission.UpdateObject
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.Permission,
|
||||
core.space.Model,
|
||||
{
|
||||
label: core.string.DeleteObject,
|
||||
description: core.string.DeleteObjectDescription
|
||||
},
|
||||
core.permission.DeleteObject
|
||||
)
|
||||
}
|
@ -21,11 +21,30 @@ import {
|
||||
DOMAIN_MODEL,
|
||||
IndexKind,
|
||||
type Ref,
|
||||
type Space
|
||||
type Space,
|
||||
type TypedSpace,
|
||||
type SpaceType,
|
||||
type SpaceTypeDescriptor,
|
||||
type Role,
|
||||
type Class,
|
||||
type Permission,
|
||||
type CollectionSize
|
||||
} from '@hcengineering/core'
|
||||
import { ArrOf, Hidden, Index, Model, Prop, TypeBoolean, TypeRef, TypeString, UX } from '@hcengineering/model'
|
||||
import {
|
||||
ArrOf,
|
||||
Collection,
|
||||
Hidden,
|
||||
Index,
|
||||
Model,
|
||||
Prop,
|
||||
TypeBoolean,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
UX
|
||||
} from '@hcengineering/model'
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import core from './component'
|
||||
import { TDoc } from './core'
|
||||
import { TDoc, TAttachedDoc } from './core'
|
||||
|
||||
export const DOMAIN_SPACE = 'space' as Domain
|
||||
|
||||
@ -53,6 +72,75 @@ export class TSpace extends TDoc implements Space {
|
||||
members!: Arr<Ref<Account>>
|
||||
}
|
||||
|
||||
@Model(core.class.TypedSpace, core.class.Space)
|
||||
@UX(core.string.TypedSpace, undefined, undefined, 'name')
|
||||
export class TTypedSpace extends TSpace implements TypedSpace {
|
||||
@Prop(TypeRef(core.class.SpaceType), core.string.SpaceType)
|
||||
type!: Ref<SpaceType>
|
||||
}
|
||||
|
||||
@Model(core.class.SpaceTypeDescriptor, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TSpaceTypeDescriptor extends TDoc implements SpaceTypeDescriptor {
|
||||
name!: IntlString
|
||||
description!: IntlString
|
||||
icon!: Asset
|
||||
baseClass!: Ref<Class<Space>>
|
||||
availablePermissions!: Ref<Permission>[]
|
||||
}
|
||||
|
||||
@Model(core.class.SpaceType, core.class.Doc, DOMAIN_MODEL)
|
||||
@UX(core.string.SpaceType, undefined, undefined, 'name')
|
||||
export class TSpaceType extends TDoc implements SpaceType {
|
||||
@Prop(TypeString(), core.string.Name)
|
||||
@Index(IndexKind.FullText)
|
||||
name!: string
|
||||
|
||||
@Prop(TypeString(), core.string.ShortDescription)
|
||||
shortDescription?: string
|
||||
|
||||
@Prop(TypeRef(core.class.SpaceTypeDescriptor), core.string.Descriptor)
|
||||
descriptor!: Ref<SpaceTypeDescriptor>
|
||||
|
||||
@Prop(TypeRef(core.class.Class), core.string.TargetClass)
|
||||
targetClass!: Ref<Class<Space>>
|
||||
|
||||
@Prop(Collection(core.class.Role), core.string.Roles)
|
||||
roles!: CollectionSize<Role>
|
||||
}
|
||||
|
||||
@Model(core.class.Role, core.class.AttachedDoc, DOMAIN_MODEL)
|
||||
@UX(core.string.Role, undefined, undefined, 'name')
|
||||
export class TRole extends TAttachedDoc implements Role {
|
||||
@Prop(TypeRef(core.class.SpaceType), core.string.AttachedTo)
|
||||
@Index(IndexKind.Indexed)
|
||||
@Hidden()
|
||||
declare attachedTo: Ref<SpaceType>
|
||||
|
||||
@Prop(TypeRef(core.class.SpaceType), core.string.AttachedToClass)
|
||||
@Index(IndexKind.Indexed)
|
||||
@Hidden()
|
||||
declare attachedToClass: Ref<Class<SpaceType>>
|
||||
|
||||
@Prop(TypeString(), core.string.Collection)
|
||||
@Hidden()
|
||||
override collection: 'roles' = 'roles'
|
||||
|
||||
@Prop(TypeString(), core.string.Name)
|
||||
@Index(IndexKind.FullText)
|
||||
name!: string
|
||||
|
||||
@Prop(ArrOf(TypeRef(core.class.Permission)), core.string.Permission)
|
||||
permissions!: Ref<Permission>[]
|
||||
}
|
||||
|
||||
@Model(core.class.Permission, core.class.Doc, DOMAIN_MODEL)
|
||||
@UX(core.string.Permission)
|
||||
export class TPermission extends TDoc implements Permission {
|
||||
label!: IntlString
|
||||
description?: IntlString
|
||||
icon?: Asset
|
||||
}
|
||||
|
||||
@Model(core.class.Account, core.class.Doc, DOMAIN_MODEL)
|
||||
@UX(core.string.Account)
|
||||
export class TAccount extends TDoc implements Account {
|
||||
|
@ -40,7 +40,7 @@ import {
|
||||
} from '@hcengineering/model'
|
||||
import attachment, { TAttachment } from '@hcengineering/model-attachment'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import core, { TAttachedDoc, TSpace } from '@hcengineering/model-core'
|
||||
import core, { TAttachedDoc, TTypedSpace } from '@hcengineering/model-core'
|
||||
import { createPublicLinkAction } from '@hcengineering/model-guest'
|
||||
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
@ -78,6 +78,11 @@ export class TDocument extends TAttachedDoc implements Document {
|
||||
@Hidden()
|
||||
declare attachedToClass: Ref<Class<Document>>
|
||||
|
||||
@Prop(TypeRef(core.class.Space), core.string.Space)
|
||||
@Index(IndexKind.Indexed)
|
||||
@Hidden()
|
||||
declare space: Ref<Teamspace>
|
||||
|
||||
@Prop(TypeString(), core.string.Collection)
|
||||
@Hidden()
|
||||
override collection: 'children' = 'children'
|
||||
@ -129,6 +134,11 @@ export class TDocumentSnapshot extends TAttachedDoc implements DocumentSnapshot
|
||||
@Prop(TypeRef(core.class.Class), core.string.AttachedToClass)
|
||||
declare attachedToClass: Ref<Class<Document>>
|
||||
|
||||
@Prop(TypeRef(core.class.Space), core.string.Space)
|
||||
@Index(IndexKind.Indexed)
|
||||
@Hidden()
|
||||
declare space: Ref<Teamspace>
|
||||
|
||||
@Prop(TypeString(), core.string.Collection)
|
||||
@Hidden()
|
||||
override collection: 'snapshots' = 'snapshots'
|
||||
@ -148,13 +158,26 @@ export class TSavedDocument extends TPreference implements SavedDocument {
|
||||
declare attachedTo: Ref<Document>
|
||||
}
|
||||
|
||||
@Model(document.class.Teamspace, core.class.Space)
|
||||
@Model(document.class.Teamspace, core.class.TypedSpace)
|
||||
@UX(document.string.Teamspace, document.icon.Teamspace, 'Teamspace', 'name')
|
||||
export class TTeamspace extends TSpace implements Teamspace {}
|
||||
export class TTeamspace extends TTypedSpace implements Teamspace {}
|
||||
|
||||
function defineTeamspace (builder: Builder): void {
|
||||
builder.createModel(TTeamspace)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.SpaceTypeDescriptor,
|
||||
core.space.Model,
|
||||
{
|
||||
name: document.string.DocumentApplication,
|
||||
description: document.string.Description,
|
||||
icon: document.icon.Document,
|
||||
baseClass: document.class.Teamspace,
|
||||
availablePermissions: [core.permission.DeleteObject]
|
||||
},
|
||||
document.descriptor.TeamspaceType
|
||||
)
|
||||
|
||||
// Navigator
|
||||
|
||||
builder.mixin(document.class.Teamspace, core.class.Class, view.mixin.SpacePresenter, {
|
||||
|
@ -25,6 +25,8 @@ import {
|
||||
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
|
||||
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
|
||||
import { type Asset } from '@hcengineering/platform'
|
||||
import { createSpaceType } from '@hcengineering/setting'
|
||||
|
||||
import document, { documentId, DOMAIN_DOCUMENT } from './index'
|
||||
|
||||
async function createSpace (tx: TxOperations): Promise<void> {
|
||||
@ -238,6 +240,40 @@ async function setNoParent (client: MigrationClient): Promise<void> {
|
||||
)
|
||||
}
|
||||
|
||||
async function createDefaultTeamspaceType (tx: TxOperations): Promise<void> {
|
||||
const exist = await tx.findOne(core.class.SpaceType, { _id: document.spaceType.DefaultTeamspaceType })
|
||||
const deleted = await tx.findOne(core.class.TxRemoveDoc, {
|
||||
objectId: document.spaceType.DefaultTeamspaceType
|
||||
})
|
||||
|
||||
if (exist === undefined && deleted === undefined) {
|
||||
await createSpaceType(
|
||||
tx,
|
||||
{
|
||||
name: 'Default teamspace type',
|
||||
descriptor: document.descriptor.TeamspaceType,
|
||||
roles: 0
|
||||
},
|
||||
document.spaceType.DefaultTeamspaceType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateTeamspaces (client: MigrationClient): Promise<void> {
|
||||
await client.update(
|
||||
DOMAIN_SPACE,
|
||||
{
|
||||
_class: document.class.Teamspace,
|
||||
type: { $exists: false }
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
type: document.spaceType.DefaultTeamspaceType
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const documentOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await setNoParent(client)
|
||||
@ -263,10 +299,12 @@ export const documentOperation: MigrateOperation = {
|
||||
func: migrateDocumentIcons
|
||||
}
|
||||
])
|
||||
await migrateTeamspaces(client)
|
||||
},
|
||||
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
await createSpace(tx)
|
||||
await createDefaultTeamspaceType(tx)
|
||||
}
|
||||
}
|
||||
|
@ -672,6 +672,7 @@ export function createModel (builder: Builder): void {
|
||||
description: lead.string.ManageFunnelStatuses,
|
||||
icon: lead.icon.LeadApplication,
|
||||
baseClass: lead.class.Funnel,
|
||||
availablePermissions: [core.permission.DeleteObject],
|
||||
allowedTaskTypeDescriptors: [lead.descriptors.Lead]
|
||||
},
|
||||
lead.descriptors.FunnelType
|
||||
|
@ -39,6 +39,7 @@ async function createSpace (tx: TxOperations): Promise<void> {
|
||||
descriptor: lead.descriptors.FunnelType,
|
||||
description: '',
|
||||
tasks: [],
|
||||
roles: 0,
|
||||
classic: false
|
||||
},
|
||||
[
|
||||
|
@ -21,7 +21,7 @@ import lead from '@hcengineering/lead-resources/src/plugin'
|
||||
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import { type TaskTypeDescriptor, type ProjectType } from '@hcengineering/task'
|
||||
import { type TaskTypeDescriptor } from '@hcengineering/task'
|
||||
import type { AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import { type Action, type ActionCategory, type Viewlet } from '@hcengineering/view'
|
||||
|
||||
@ -48,9 +48,6 @@ export default mergeIds(leadId, lead, {
|
||||
space: {
|
||||
DefaultFunnel: '' as Ref<Funnel>
|
||||
},
|
||||
template: {
|
||||
DefaultFunnel: '' as Ref<ProjectType>
|
||||
},
|
||||
viewlet: {
|
||||
TableCustomer: '' as Ref<Viewlet>,
|
||||
TableLead: '' as Ref<Viewlet>,
|
||||
|
@ -1709,6 +1709,7 @@ export function createModel (builder: Builder): void {
|
||||
icon: recruit.icon.RecruitApplication,
|
||||
editor: recruit.component.VacancyTemplateEditor,
|
||||
baseClass: recruit.class.Vacancy,
|
||||
availablePermissions: [core.permission.DeleteObject],
|
||||
allowedTaskTypeDescriptors: [recruit.descriptors.Application]
|
||||
},
|
||||
recruit.descriptors.VacancyType
|
||||
|
@ -157,6 +157,7 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<Proje
|
||||
descriptor: recruit.descriptors.VacancyType,
|
||||
description: '',
|
||||
tasks: [],
|
||||
roles: 0,
|
||||
classic: false
|
||||
},
|
||||
[
|
||||
|
@ -30,7 +30,10 @@ import {
|
||||
type InviteSettings,
|
||||
type WorkspaceSetting,
|
||||
type SettingsCategory,
|
||||
type UserMixin
|
||||
type UserMixin,
|
||||
type SpaceTypeEditor,
|
||||
type SpaceTypeEditorSection,
|
||||
type SpaceTypeCreator
|
||||
} from '@hcengineering/setting'
|
||||
import templates from '@hcengineering/templates'
|
||||
import setting from './plugin'
|
||||
@ -105,6 +108,17 @@ export class TWorkspaceSetting extends TDoc implements WorkspaceSetting {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
@Mixin(setting.mixin.SpaceTypeEditor, core.class.Class)
|
||||
export class TSpaceTypeEditor extends TClass implements SpaceTypeEditor {
|
||||
sections!: SpaceTypeEditorSection[]
|
||||
subEditors?: Record<string, AnyComponent>
|
||||
}
|
||||
|
||||
@Mixin(setting.mixin.SpaceTypeCreator, core.class.Class)
|
||||
export class TSpaceTypeCreator extends TClass implements SpaceTypeCreator {
|
||||
extraComponent!: AnyComponent
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TIntegration,
|
||||
@ -114,7 +128,9 @@ export function createModel (builder: Builder): void {
|
||||
TEditable,
|
||||
TUserMixin,
|
||||
TInviteSettings,
|
||||
TWorkspaceSetting
|
||||
TWorkspaceSetting,
|
||||
TSpaceTypeEditor,
|
||||
TSpaceTypeCreator
|
||||
)
|
||||
|
||||
builder.mixin(setting.class.Integration, core.class.Class, notification.mixin.ClassCollaborators, {
|
||||
@ -560,4 +576,56 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
setting.ids.IntegrationDisabledNotification
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
setting.class.SettingsCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
name: 'spaceTypes',
|
||||
label: setting.string.SpaceTypes,
|
||||
icon: setting.icon.Privacy, // TODO: update icon. Where is it displayed?
|
||||
component: setting.component.ManageSpaceTypeContent,
|
||||
extraComponents: {
|
||||
navigation: setting.component.ManageSpaceTypes,
|
||||
tools: setting.component.ManageSpaceTypesTools
|
||||
},
|
||||
group: 'settings-editor',
|
||||
secured: false,
|
||||
order: 6000,
|
||||
expandable: true
|
||||
},
|
||||
setting.ids.ManageSpaces
|
||||
)
|
||||
|
||||
builder.mixin(core.class.SpaceType, core.class.Class, setting.mixin.SpaceTypeEditor, {
|
||||
sections: [
|
||||
{
|
||||
id: 'general',
|
||||
label: setting.string.General,
|
||||
component: setting.component.SpaceTypeGeneralSectionEditor,
|
||||
withoutContainer: true
|
||||
},
|
||||
{
|
||||
id: 'properties',
|
||||
label: setting.string.Properties,
|
||||
component: setting.component.SpaceTypePropertiesSectionEditor
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
label: setting.string.Roles,
|
||||
component: setting.component.SpaceTypeRolesSectionEditor
|
||||
}
|
||||
],
|
||||
subEditors: {
|
||||
roles: setting.component.RoleEditor
|
||||
}
|
||||
})
|
||||
|
||||
builder.mixin(core.class.SpaceTypeDescriptor, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: setting.component.SpaceTypeDescriptorPresenter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.Permission, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: setting.component.PermissionPresenter
|
||||
})
|
||||
}
|
||||
|
@ -53,7 +53,15 @@ import {
|
||||
} from '@hcengineering/model'
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import core, { DOMAIN_SPACE, TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
|
||||
import core, {
|
||||
DOMAIN_SPACE,
|
||||
TAttachedDoc,
|
||||
TClass,
|
||||
TDoc,
|
||||
TSpaceType,
|
||||
TSpaceTypeDescriptor,
|
||||
TTypedSpace
|
||||
} from '@hcengineering/model-core'
|
||||
import view, {
|
||||
classPresenter,
|
||||
createAction,
|
||||
@ -173,18 +181,20 @@ export class TProjectTypeClass extends TClass implements ProjectTypeClass {
|
||||
projectType!: Ref<ProjectType>
|
||||
}
|
||||
|
||||
@Model(task.class.Project, core.class.Space)
|
||||
export class TProject extends TSpace implements Project {
|
||||
@Model(task.class.Project, core.class.TypedSpace)
|
||||
export class TProject extends TTypedSpace implements Project {
|
||||
@Prop(TypeRef(task.class.ProjectType), task.string.ProjectType)
|
||||
type!: Ref<ProjectType>
|
||||
declare type: Ref<ProjectType>
|
||||
}
|
||||
|
||||
@Model(task.class.ProjectType, core.class.Space)
|
||||
export class TProjectType extends TSpace implements ProjectType {
|
||||
shortDescription?: string
|
||||
|
||||
@Model(task.class.ProjectType, core.class.SpaceType)
|
||||
export class TProjectType extends TSpaceType implements ProjectType {
|
||||
@Prop(TypeRef(task.class.ProjectTypeDescriptor), getEmbeddedLabel('Descriptor'))
|
||||
descriptor!: Ref<ProjectTypeDescriptor>
|
||||
declare descriptor: Ref<ProjectTypeDescriptor>
|
||||
|
||||
@Prop(TypeString(), task.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: string
|
||||
|
||||
@Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Tasks'))
|
||||
tasks!: Ref<TaskType>[]
|
||||
@ -193,13 +203,13 @@ export class TProjectType extends TSpace implements ProjectType {
|
||||
statuses!: ProjectStatus[]
|
||||
|
||||
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Target Class'))
|
||||
targetClass!: Ref<Class<Project>>
|
||||
declare targetClass: Ref<Class<Project>>
|
||||
|
||||
@Prop(TypeBoolean(), getEmbeddedLabel('Classic'))
|
||||
classic!: boolean
|
||||
}
|
||||
|
||||
@Model(task.class.TaskType, core.class.Doc, DOMAIN_TASK)
|
||||
@Model(task.class.TaskType, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TTaskType extends TDoc implements TaskType {
|
||||
@Prop(TypeString(), getEmbeddedLabel('Name'))
|
||||
name!: string
|
||||
@ -232,15 +242,12 @@ export class TTaskType extends TDoc implements TaskType {
|
||||
statusCategories!: Ref<StatusCategory>[]
|
||||
}
|
||||
|
||||
@Model(task.class.ProjectTypeDescriptor, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TProjectTypeDescriptor extends TDoc implements ProjectTypeDescriptor {
|
||||
name!: IntlString
|
||||
description!: IntlString
|
||||
icon!: Asset
|
||||
@Model(task.class.ProjectTypeDescriptor, core.class.SpaceTypeDescriptor, DOMAIN_MODEL)
|
||||
export class TProjectTypeDescriptor extends TSpaceTypeDescriptor implements ProjectTypeDescriptor {
|
||||
editor?: AnyComponent
|
||||
allowedClassic?: boolean
|
||||
allowedTaskTypeDescriptors?: Ref<TaskTypeDescriptor>[] // if undefined we allow all possible
|
||||
baseClass!: Ref<Class<Task>>
|
||||
declare baseClass: Ref<Class<Project>>
|
||||
}
|
||||
|
||||
@Model(task.class.Sequence, core.class.Doc, DOMAIN_KANBAN)
|
||||
@ -540,25 +547,49 @@ export function createModel (builder: Builder): void {
|
||||
value: true
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
setting.class.SettingsCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
name: 'statuses',
|
||||
label: task.string.ManageProjects,
|
||||
icon: task.icon.ManageTemplates,
|
||||
component: task.component.ManageProjectsContent,
|
||||
extraComponents: {
|
||||
navigation: task.component.ManageProjects,
|
||||
tools: task.component.ManageProjectsTools
|
||||
builder.mixin(task.class.ProjectTypeDescriptor, core.class.Class, setting.mixin.SpaceTypeCreator, {
|
||||
extraComponent: task.component.CreateProjectType
|
||||
})
|
||||
|
||||
builder.mixin(task.class.ProjectType, core.class.Class, setting.mixin.SpaceTypeEditor, {
|
||||
sections: [
|
||||
{
|
||||
id: 'general',
|
||||
label: setting.string.General,
|
||||
component: task.component.ProjectTypeGeneralSectionEditor,
|
||||
withoutContainer: true
|
||||
},
|
||||
group: 'settings-editor',
|
||||
secured: false,
|
||||
order: 6000,
|
||||
expandable: true
|
||||
},
|
||||
task.ids.ManageProjects
|
||||
)
|
||||
{
|
||||
id: 'properties',
|
||||
label: setting.string.Properties,
|
||||
component: setting.component.SpaceTypePropertiesSectionEditor
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
label: setting.string.Roles,
|
||||
component: setting.component.SpaceTypeRolesSectionEditor
|
||||
},
|
||||
{
|
||||
id: 'taskTypes',
|
||||
label: setting.string.TaskTypes,
|
||||
component: task.component.ProjectTypeTasksTypeSectionEditor
|
||||
},
|
||||
{
|
||||
id: 'automations',
|
||||
label: setting.string.Automations,
|
||||
component: task.component.ProjectTypeAutomationsSectionEditor
|
||||
},
|
||||
{
|
||||
id: 'collections',
|
||||
label: setting.string.Collections,
|
||||
component: task.component.ProjectTypeCollectionsSectionEditor
|
||||
}
|
||||
],
|
||||
subEditors: {
|
||||
taskTypes: task.component.TaskTypeEditor,
|
||||
roles: setting.component.RoleEditor
|
||||
}
|
||||
})
|
||||
|
||||
createPublicLinkAction(builder, task.class.Task, task.action.PublicLink)
|
||||
}
|
||||
@ -589,8 +620,7 @@ export async function fixTaskTypes (
|
||||
throw new Error('category is not found in model')
|
||||
}
|
||||
|
||||
const projectTypes = await client.find<ProjectType>(DOMAIN_SPACE, {
|
||||
_class: task.class.ProjectType,
|
||||
const projectTypes = await client.model.findAll(task.class.ProjectType, {
|
||||
descriptor
|
||||
})
|
||||
const baseClassClass = client.hierarchy.getClass(categoryObj.baseClass)
|
||||
@ -751,7 +781,7 @@ export async function fixTaskTypes (
|
||||
parent: t._id,
|
||||
_id: taskTypeId,
|
||||
_class: task.class.TaskType,
|
||||
space: t._id,
|
||||
space: core.space.Model,
|
||||
statuses: dStatuses,
|
||||
modifiedBy: core.account.System,
|
||||
modifiedOn: Date.now(),
|
||||
|
@ -13,7 +13,17 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { ClassifierKind, TxOperations, toIdMap, type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import {
|
||||
ClassifierKind,
|
||||
TxOperations,
|
||||
toIdMap,
|
||||
type Class,
|
||||
type Doc,
|
||||
type Ref,
|
||||
type TxCreateDoc,
|
||||
generateId,
|
||||
DOMAIN_TX
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
createOrUpdate,
|
||||
tryMigrate,
|
||||
@ -25,7 +35,7 @@ import {
|
||||
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
|
||||
import tags from '@hcengineering/model-tags'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { taskId, type TaskType } from '@hcengineering/task'
|
||||
import { type ProjectType, taskId, type TaskType } from '@hcengineering/task'
|
||||
import { DOMAIN_TASK } from '.'
|
||||
import task from './plugin'
|
||||
|
||||
@ -167,6 +177,79 @@ async function fixProjectTypeMissingClass (client: MigrationUpgradeClient): Prom
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateProjectTypes (client: MigrationClient): Promise<void> {
|
||||
const oldProjectTypes = await client.find<ProjectType>(DOMAIN_SPACE, { _class: task.class.ProjectType })
|
||||
|
||||
for (const pt of oldProjectTypes) {
|
||||
// Create the project type in model instead
|
||||
const tx: TxCreateDoc<ProjectType> = {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxCreateDoc,
|
||||
space: core.space.Tx,
|
||||
objectClass: pt._class,
|
||||
objectSpace: core.space.Model,
|
||||
objectId: pt._id,
|
||||
attributes: {
|
||||
descriptor: pt.descriptor,
|
||||
tasks: pt.tasks,
|
||||
statuses: pt.statuses,
|
||||
targetClass: pt.targetClass,
|
||||
classic: pt.classic,
|
||||
name: pt.name,
|
||||
description: pt.description,
|
||||
shortDescription: pt.shortDescription,
|
||||
roles: pt.roles ?? []
|
||||
},
|
||||
modifiedOn: pt.modifiedOn,
|
||||
createdBy: pt.createdBy,
|
||||
createdOn: pt.createdOn,
|
||||
modifiedBy: pt.modifiedBy
|
||||
}
|
||||
await client.create(DOMAIN_TX, tx)
|
||||
|
||||
// Remove old project type from spaces
|
||||
await client.delete(DOMAIN_SPACE, pt._id)
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateTaskTypes (client: MigrationClient): Promise<void> {
|
||||
const oldTaskTypes = await client.find<TaskType>(DOMAIN_TASK, { _class: task.class.TaskType })
|
||||
|
||||
for (const tt of oldTaskTypes) {
|
||||
// Create the task type in model instead
|
||||
const tx: TxCreateDoc<TaskType> = {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxCreateDoc,
|
||||
space: core.space.Tx,
|
||||
objectClass: tt._class,
|
||||
objectSpace: core.space.Model,
|
||||
objectId: tt._id,
|
||||
attributes: {
|
||||
parent: tt.parent,
|
||||
descriptor: tt.descriptor,
|
||||
name: tt.name,
|
||||
kind: tt.kind,
|
||||
ofClass: tt.ofClass,
|
||||
targetClass: tt.targetClass,
|
||||
statuses: tt.statuses,
|
||||
statusClass: tt.statusClass,
|
||||
statusCategories: tt.statusCategories,
|
||||
allowedAsChildOf: tt.allowedAsChildOf,
|
||||
icon: tt.icon,
|
||||
color: tt.color
|
||||
},
|
||||
modifiedOn: tt.modifiedOn,
|
||||
createdBy: tt.createdBy,
|
||||
createdOn: tt.createdOn,
|
||||
modifiedBy: tt.modifiedBy
|
||||
}
|
||||
await client.create(DOMAIN_TX, tx)
|
||||
|
||||
// Remove old task type from task
|
||||
await client.delete(DOMAIN_TASK, tt._id)
|
||||
}
|
||||
}
|
||||
|
||||
export const taskOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, taskId, [
|
||||
@ -201,6 +284,8 @@ export const taskOperation: MigrateOperation = {
|
||||
}
|
||||
}
|
||||
])
|
||||
await migrateTaskTypes(client)
|
||||
await migrateProjectTypes(client)
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
|
@ -61,10 +61,7 @@ export default mergeIds(taskId, task, {
|
||||
TaskTypePresenter: '' as AnyComponent,
|
||||
ProjectTypePresenter: '' as AnyComponent,
|
||||
TaskTypeClassPresenter: '' as AnyComponent,
|
||||
ProjectTypeClassPresenter: '' as AnyComponent,
|
||||
ManageProjects: '' as AnyComponent,
|
||||
ManageProjectsTools: '' as AnyComponent,
|
||||
ManageProjectsContent: '' as AnyComponent
|
||||
ProjectTypeClassPresenter: '' as AnyComponent
|
||||
},
|
||||
space: {
|
||||
TasksPublic: '' as Ref<Space>
|
||||
|
@ -182,6 +182,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
||||
mode: ['context', 'browser'],
|
||||
group: 'remove'
|
||||
},
|
||||
visibilityTester: view.function.CanDeleteObject,
|
||||
override: [view.action.Delete]
|
||||
},
|
||||
tracker.action.DeleteIssue
|
||||
@ -709,7 +710,8 @@ export function createActions (builder: Builder, issuesId: string, componentsId:
|
||||
category: tracker.category.Tracker,
|
||||
input: 'any',
|
||||
target: tracker.class.Milestone,
|
||||
context: { mode: ['context', 'browser'], group: 'remove' }
|
||||
context: { mode: ['context', 'browser'], group: 'remove' },
|
||||
visibilityTester: view.function.CanDeleteObject
|
||||
},
|
||||
tracker.action.DeleteMilestone
|
||||
)
|
||||
|
@ -687,6 +687,7 @@ export function createModel (builder: Builder): void {
|
||||
description: tracker.string.ManageWorkflowStatuses,
|
||||
icon: task.icon.Task,
|
||||
baseClass: tracker.class.Project,
|
||||
availablePermissions: [core.permission.DeleteObject],
|
||||
allowedClassic: true,
|
||||
allowedTaskTypeDescriptors: [tracker.descriptors.Issue]
|
||||
},
|
||||
|
@ -66,6 +66,7 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
|
||||
descriptor: tracker.descriptors.ProjectType,
|
||||
description: '',
|
||||
tasks: [],
|
||||
roles: 0,
|
||||
classic: true
|
||||
},
|
||||
[
|
||||
|
@ -624,7 +624,8 @@ export function createModel (builder: Builder): void {
|
||||
category: view.category.General,
|
||||
input: 'any',
|
||||
target: core.class.Doc,
|
||||
context: { mode: ['context', 'browser'], group: 'remove' }
|
||||
context: { mode: ['context', 'browser'], group: 'remove' },
|
||||
visibilityTester: view.function.CanDeleteObject
|
||||
},
|
||||
view.action.Delete
|
||||
)
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Ref } from '@hcengineering/core'
|
||||
import { type IntlString, mergeIds } from '@hcengineering/platform'
|
||||
import { type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import { type FilterFunction, type ViewAction, type ViewCategoryAction, viewId } from '@hcengineering/view'
|
||||
import { type PresentationMiddlewareFactory } from '@hcengineering/presentation/src/pipeline'
|
||||
@ -126,7 +126,8 @@ export default mergeIds(viewId, view, {
|
||||
FilterDateNextMonth: '' as FilterFunction,
|
||||
FilterDateNotSpecified: '' as FilterFunction,
|
||||
FilterDateCustom: '' as FilterFunction,
|
||||
ShowEmptyGroups: '' as ViewCategoryAction
|
||||
ShowEmptyGroups: '' as ViewCategoryAction,
|
||||
CanDeleteObject: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
|
||||
},
|
||||
pipeline: {
|
||||
PresentationMiddleware: '' as Ref<PresentationMiddlewareFactory>,
|
||||
|
@ -2,6 +2,8 @@
|
||||
"string": {
|
||||
"Id": "Id",
|
||||
"Space": "Space",
|
||||
"TypedSpace": "Typed space",
|
||||
"SpaceType": "Space type",
|
||||
"Modified": "Modified",
|
||||
"ModifiedDate": "Modified date",
|
||||
"ModifiedBy": "Modified by",
|
||||
@ -10,6 +12,11 @@
|
||||
"AttachedToClass": "Attached to class",
|
||||
"Name": "Name",
|
||||
"Description": "Description",
|
||||
"ShortDescription": "Short description",
|
||||
"Descriptor": "Descriptor",
|
||||
"TargetClass": "Target class",
|
||||
"Role": "Role",
|
||||
"Roles": "Roles",
|
||||
"Private": "Private",
|
||||
"Archived": "Archived",
|
||||
"ClassLabel": "Type",
|
||||
@ -35,6 +42,11 @@
|
||||
"Status": "Status",
|
||||
"StatusCategory": "Status category",
|
||||
"Account": "Account",
|
||||
"Rank": "Rank"
|
||||
"Rank": "Rank",
|
||||
"Permission": "Permission",
|
||||
"CreateObject": "Create object",
|
||||
"UpdateObject": "Update object",
|
||||
"DeleteObject": "Delete object",
|
||||
"DeleteObjectDescription": "Grants users ability to delete objects in the space"
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
"string": {
|
||||
"Id": "Id",
|
||||
"Space": "Пространство",
|
||||
"TypedSpace": "Типизированное пространство",
|
||||
"SpaceType": "Тип пространства",
|
||||
"Modified": "Изменено",
|
||||
"ModifiedDate": "Дата изменения",
|
||||
"ModifiedBy": "Изменен",
|
||||
@ -10,6 +12,11 @@
|
||||
"AttachedToClass": "Прикреплен к классу",
|
||||
"Name": "Название",
|
||||
"Description": "Описание",
|
||||
"ShortDescription": "Короткое описание",
|
||||
"Descriptor": "Дескриптор",
|
||||
"TargetClass": "Целевой класс",
|
||||
"Role": "Роль",
|
||||
"Roles": "Роли",
|
||||
"Private": "Личный",
|
||||
"Archived": "Архивный",
|
||||
"ClassLabel": "Тип",
|
||||
@ -35,6 +42,11 @@
|
||||
"Status": "Статус",
|
||||
"StatusCategory": "Категория статуса",
|
||||
"Account": "Аккаунт",
|
||||
"Rank": "Ранг"
|
||||
"Rank": "Ранг",
|
||||
"Permission": "Разрешение",
|
||||
"CreateObject": "Создавать объект",
|
||||
"UpdateObject": "Обновлять объект",
|
||||
"DeleteObject": "Удалять объект",
|
||||
"DeleteObjectDescription": "Дает пользователям разрешение удалять объекты в пространстве"
|
||||
}
|
||||
}
|
||||
|
@ -362,6 +362,66 @@ export interface Space extends Doc {
|
||||
archived: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Space with custom configured type
|
||||
*/
|
||||
export interface TypedSpace extends Space {
|
||||
type: Ref<SpaceType>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Is used to describe "types" for space type
|
||||
*/
|
||||
export interface SpaceTypeDescriptor extends Doc {
|
||||
name: IntlString
|
||||
description: IntlString
|
||||
icon: Asset
|
||||
baseClass: Ref<Class<Space>> // Child class of Space for which the space type can be defined
|
||||
availablePermissions: Ref<Permission>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Customisable space type allowing to configure space roles and permissions within them
|
||||
*/
|
||||
export interface SpaceType extends Doc {
|
||||
name: string
|
||||
shortDescription?: string
|
||||
descriptor: Ref<SpaceTypeDescriptor>
|
||||
targetClass: Ref<Class<Space>> // A dynamic mixin for Spaces to hold custom attributes and roles assignment of the space type
|
||||
roles: CollectionSize<Role>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Role defines permissions for employees assigned to this role within the space
|
||||
*/
|
||||
export interface Role extends AttachedDoc<SpaceType, 'roles'> {
|
||||
name: string
|
||||
permissions: Ref<Permission>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Defines assignment of employees to a role within a space
|
||||
*/
|
||||
export type RolesAssignment = Record<Ref<Role>, Ref<Account>[] | undefined>
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Permission is a basic access control item in the system
|
||||
*/
|
||||
export interface Permission extends Doc {
|
||||
label: IntlString
|
||||
description?: IntlString
|
||||
icon?: Asset
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -38,14 +38,19 @@ import type {
|
||||
Markup,
|
||||
MigrationState,
|
||||
Obj,
|
||||
Permission,
|
||||
PluginConfiguration,
|
||||
Ref,
|
||||
RefTo,
|
||||
RelatedDocument,
|
||||
Role,
|
||||
Space,
|
||||
SpaceType,
|
||||
SpaceTypeDescriptor,
|
||||
Timestamp,
|
||||
Type,
|
||||
TypeAny,
|
||||
TypedSpace,
|
||||
UserStatus
|
||||
} from './classes'
|
||||
import { CollaborativeDoc } from './collaboration'
|
||||
@ -93,6 +98,11 @@ export default plugin(coreId, {
|
||||
TxUpdateDoc: '' as Ref<Class<TxUpdateDoc<Doc>>>,
|
||||
TxRemoveDoc: '' as Ref<Class<TxRemoveDoc<Doc>>>,
|
||||
Space: '' as Ref<Class<Space>>,
|
||||
TypedSpace: '' as Ref<Class<TypedSpace>>,
|
||||
SpaceTypeDescriptor: '' as Ref<Class<SpaceTypeDescriptor>>,
|
||||
SpaceType: '' as Ref<Class<SpaceType>>,
|
||||
Role: '' as Ref<Class<Role>>,
|
||||
Permission: '' as Ref<Class<Permission>>,
|
||||
Account: '' as Ref<Class<Account>>,
|
||||
Type: '' as Ref<Class<Type<any>>>,
|
||||
TypeString: '' as Ref<Class<Type<string>>>,
|
||||
@ -157,6 +167,8 @@ export default plugin(coreId, {
|
||||
string: {
|
||||
Id: '' as IntlString,
|
||||
Space: '' as IntlString,
|
||||
TypedSpace: '' as IntlString,
|
||||
SpaceType: '' as IntlString,
|
||||
Modified: '' as IntlString,
|
||||
ModifiedDate: '' as IntlString,
|
||||
ModifiedBy: '' as IntlString,
|
||||
@ -180,6 +192,11 @@ export default plugin(coreId, {
|
||||
Name: '' as IntlString,
|
||||
Enum: '' as IntlString,
|
||||
Description: '' as IntlString,
|
||||
ShortDescription: '' as IntlString,
|
||||
Descriptor: '' as IntlString,
|
||||
TargetClass: '' as IntlString,
|
||||
Role: '' as IntlString,
|
||||
Roles: '' as IntlString,
|
||||
Hyperlink: '' as IntlString,
|
||||
Private: '' as IntlString,
|
||||
Object: '' as IntlString,
|
||||
@ -189,6 +206,16 @@ export default plugin(coreId, {
|
||||
Status: '' as IntlString,
|
||||
Account: '' as IntlString,
|
||||
StatusCategory: '' as IntlString,
|
||||
Rank: '' as IntlString
|
||||
Rank: '' as IntlString,
|
||||
Permission: '' as IntlString,
|
||||
CreateObject: '' as IntlString,
|
||||
UpdateObject: '' as IntlString,
|
||||
DeleteObject: '' as IntlString,
|
||||
DeleteObjectDescription: '' as IntlString
|
||||
},
|
||||
permission: {
|
||||
CreateObject: '' as Ref<Permission>,
|
||||
UpdateObject: '' as Ref<Permission>,
|
||||
DeleteObject: '' as Ref<Permission>
|
||||
}
|
||||
})
|
||||
|
@ -14,11 +14,26 @@
|
||||
//
|
||||
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { Account, AnyAttribute, Class, Doc, DocData, DocIndexState, IndexKind, Obj, Ref, Space } from './classes'
|
||||
import {
|
||||
Account,
|
||||
AnyAttribute,
|
||||
Class,
|
||||
Doc,
|
||||
DocData,
|
||||
DocIndexState,
|
||||
IndexKind,
|
||||
Obj,
|
||||
Permission,
|
||||
Ref,
|
||||
Role,
|
||||
Space,
|
||||
TypedSpace
|
||||
} from './classes'
|
||||
import core from './component'
|
||||
import { Hierarchy } from './hierarchy'
|
||||
import { isPredicate } from './predicate'
|
||||
import { DocumentQuery, FindResult } from './storage'
|
||||
import { TxOperations } from './operations'
|
||||
|
||||
function toHex (value: number, chars: number): string {
|
||||
const result = value.toString(16)
|
||||
@ -542,3 +557,30 @@ export const isEnum =
|
||||
(token: any): token is T[keyof T] => {
|
||||
return typeof token === 'string' && Object.values(e as Record<string, any>).includes(token)
|
||||
}
|
||||
|
||||
export async function checkPermission (
|
||||
client: TxOperations,
|
||||
_id: Ref<Permission>,
|
||||
_space: Ref<TypedSpace>
|
||||
): Promise<boolean> {
|
||||
const space = await client.findOne(core.class.TypedSpace, { _id: _space })
|
||||
const type = await client
|
||||
.getModel()
|
||||
.findOne(core.class.SpaceType, { _id: space?.type }, { lookup: { _id: { roles: core.class.Role } } })
|
||||
const mixin = type?.targetClass
|
||||
if (space === undefined || type === undefined || mixin === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const me = getCurrentAccount()
|
||||
const asMixin = client.getHierarchy().as(space, mixin)
|
||||
const myRoles = type.$lookup?.roles?.filter((role) => (asMixin as any)[role._id]?.includes(me._id)) as Role[]
|
||||
|
||||
if (myRoles === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const myPermissions = new Set(myRoles.flatMap((role) => role.permissions))
|
||||
|
||||
return myPermissions.has(_id)
|
||||
}
|
||||
|
@ -19,6 +19,8 @@
|
||||
import { showPopup, closeTooltip, Label, getIconSize2x, Loading } from '@hcengineering/ui'
|
||||
import presentation, { PDFViewer, getFileUrl } from '@hcengineering/presentation'
|
||||
import filesize from 'filesize'
|
||||
import core from '@hcengineering/core'
|
||||
import { permissionsStore } from '@hcengineering/view-resources'
|
||||
|
||||
import AttachmentName from './AttachmentName.svelte'
|
||||
|
||||
@ -35,6 +37,13 @@
|
||||
const trimFilename = (fname: string): string =>
|
||||
fname.length > maxLenght ? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2) : fname
|
||||
|
||||
$: canRemove =
|
||||
removable &&
|
||||
value !== undefined &&
|
||||
value.readonly !== true &&
|
||||
($permissionsStore.whitelist.has(value.space) ||
|
||||
$permissionsStore.ps[value.space]?.has(core.permission.DeleteObject))
|
||||
|
||||
function iconLabel (name: string): string {
|
||||
const parts = `${name}`.split('.')
|
||||
const ext = parts[parts.length - 1]
|
||||
@ -156,7 +165,7 @@
|
||||
>
|
||||
<Label label={presentation.string.Download} />
|
||||
</a>
|
||||
{#if removable && value.readonly !== true}
|
||||
{#if canRemove}
|
||||
<span>•</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
@ -29,7 +29,9 @@
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let width: string | undefined = undefined
|
||||
export let includeItems: Ref<Account>[] | undefined = undefined
|
||||
export let excludeItems: Ref<Account>[] | undefined = undefined
|
||||
export let emptyLabel: IntlString | undefined = undefined
|
||||
|
||||
let timer: any = null
|
||||
const client = getClient()
|
||||
@ -56,9 +58,7 @@
|
||||
})
|
||||
|
||||
const excludedQuery = createQuery()
|
||||
|
||||
let excluded: Account[] = []
|
||||
|
||||
$: if (excludeItems !== undefined && excludeItems.length > 0) {
|
||||
excludedQuery.query(core.class.Account, { _id: { $in: excludeItems } }, (res) => {
|
||||
excluded = res
|
||||
@ -68,21 +68,44 @@
|
||||
excluded = []
|
||||
}
|
||||
|
||||
const includedQuery = createQuery()
|
||||
let included: Account[] = []
|
||||
$: if (includeItems !== undefined && includeItems.length > 0) {
|
||||
includedQuery.query(core.class.Account, { _id: { $in: includeItems } }, (res) => {
|
||||
included = res
|
||||
})
|
||||
} else {
|
||||
includedQuery.unsubscribe()
|
||||
included = []
|
||||
}
|
||||
|
||||
$: employees = Array.from(
|
||||
(value ?? []).map((it) => $personAccountByIdStore.get(it as Ref<PersonAccount>)?.person)
|
||||
).filter((it) => it !== undefined) as Ref<Employee>[]
|
||||
|
||||
$: docQuery =
|
||||
excluded.length > 0
|
||||
? {
|
||||
_id: { $nin: excluded.map((p) => (p as PersonAccount).person as Ref<Employee>) }
|
||||
excluded.length === 0 && included.length === 0
|
||||
? {}
|
||||
: {
|
||||
_id: {
|
||||
...(included.length > 0
|
||||
? {
|
||||
$in: included.map((p) => (p as PersonAccount).person as Ref<Employee>)
|
||||
}
|
||||
: {}),
|
||||
...(excluded.length > 0
|
||||
? {
|
||||
$nin: excluded.map((p) => (p as PersonAccount).person as Ref<Employee>)
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}
|
||||
: {}
|
||||
</script>
|
||||
|
||||
<UserBoxList
|
||||
items={employees}
|
||||
{label}
|
||||
{emptyLabel}
|
||||
{readonly}
|
||||
{docQuery}
|
||||
on:update={onUpdate}
|
||||
|
@ -37,7 +37,7 @@
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||
export let emptyLabel = plugin.string.Members
|
||||
export let emptyLabel: IntlString = plugin.string.Members
|
||||
export let readonly: boolean = false
|
||||
|
||||
function filter (items: Ref<Employee>[]): Ref<Employee>[] {
|
||||
|
@ -56,6 +56,7 @@
|
||||
"ReassignToDo": "Change Todo assignee",
|
||||
"ReassignToDoConfirm": "Do you want to change Todo assignee? The Todo will be removed from the current assignee's planning.",
|
||||
"Icon": "Icon",
|
||||
"Color": "Color"
|
||||
"Color": "Color",
|
||||
"RoleLabel": "Role: {role}"
|
||||
}
|
||||
}
|
@ -56,6 +56,7 @@
|
||||
"ReassignToDo": "Изменить исполнителя Todo",
|
||||
"ReassignToDoConfirm": "Вы хотите изменить исполнителя Todo? Todo будет удалена из планирования текущего исполнителя.",
|
||||
"Icon": "Иконка",
|
||||
"Color": "Цвет"
|
||||
"Color": "Цвет",
|
||||
"RoleLabel": "Роль: {role}"
|
||||
}
|
||||
}
|
@ -62,6 +62,7 @@
|
||||
"@hcengineering/time": "^0.6.0",
|
||||
"@hcengineering/rank": "^0.6.0",
|
||||
"@tiptap/core": "^2.1.12",
|
||||
"slugify": "^1.6.6"
|
||||
"slugify": "^1.6.6",
|
||||
"fast-equals": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,21 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { AccountArrayEditor } from '@hcengineering/contact-resources'
|
||||
import core, { Account, Data, DocumentUpdate, Ref, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { Teamspace } from '@hcengineering/document'
|
||||
import core, {
|
||||
Account,
|
||||
Data,
|
||||
DocumentUpdate,
|
||||
RolesAssignment,
|
||||
Ref,
|
||||
Role,
|
||||
SpaceType,
|
||||
generateId,
|
||||
getCurrentAccount,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import document, { Teamspace } from '@hcengineering/document'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||
@ -32,15 +44,16 @@
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { IconPicker } from '@hcengineering/view-resources'
|
||||
import { IconPicker, SpaceTypeSelector } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import document from '../../plugin'
|
||||
|
||||
import documentRes from '../../plugin'
|
||||
|
||||
export let teamspace: Teamspace | undefined = undefined
|
||||
|
||||
export let namePlaceholder: string = ''
|
||||
export let descriptionPlaceholder: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
@ -52,11 +65,43 @@
|
||||
let isColorSelected = false
|
||||
let members: Ref<Account>[] =
|
||||
teamspace?.members !== undefined ? hierarchy.clone(teamspace.members) : [getCurrentAccount()._id]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let rolesAssignment: RolesAssignment = {}
|
||||
|
||||
$: isNew = teamspace === undefined
|
||||
|
||||
let typeId: Ref<SpaceType> | undefined = teamspace?.type || document.spaceType.DefaultTeamspaceType
|
||||
let spaceType: WithLookup<SpaceType> | undefined
|
||||
|
||||
$: void loadSpaceType(typeId)
|
||||
async function loadSpaceType (id: typeof typeId): Promise<void> {
|
||||
spaceType =
|
||||
id !== undefined
|
||||
? await client
|
||||
.getModel()
|
||||
.findOne(core.class.SpaceType, { _id: id }, { lookup: { _id: { roles: core.class.Role } } })
|
||||
: undefined
|
||||
|
||||
if (teamspace === undefined || spaceType?.targetClass === undefined || spaceType?.$lookup?.roles === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
rolesAssignment = getRolesAssignment()
|
||||
}
|
||||
|
||||
function getRolesAssignment (): RolesAssignment {
|
||||
if (teamspace === undefined || spaceType?.targetClass === undefined || spaceType?.$lookup?.roles === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const asMixin = hierarchy.as(teamspace, spaceType?.targetClass)
|
||||
|
||||
return spaceType.$lookup.roles.reduce<RolesAssignment>((prev, { _id }) => {
|
||||
prev[_id as Ref<Role>] = (asMixin as any)[_id]
|
||||
|
||||
return prev
|
||||
}, {})
|
||||
}
|
||||
|
||||
async function handleSave (): Promise<void> {
|
||||
if (isNew) {
|
||||
await createTeamspace()
|
||||
@ -65,7 +110,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getTeamspaceData (): Data<Teamspace> {
|
||||
function getTeamspaceData (): Omit<Data<Teamspace>, 'type'> {
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
@ -78,7 +123,7 @@
|
||||
}
|
||||
|
||||
async function updateTeamspace (): Promise<void> {
|
||||
if (teamspace === undefined) {
|
||||
if (teamspace === undefined || spaceType?.targetClass === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -114,14 +159,37 @@
|
||||
await client.update(teamspace, update)
|
||||
}
|
||||
|
||||
if (!deepEqual(rolesAssignment, getRolesAssignment())) {
|
||||
await client.updateMixin(
|
||||
teamspace._id,
|
||||
document.class.Teamspace,
|
||||
core.space.Space,
|
||||
spaceType.targetClass,
|
||||
rolesAssignment
|
||||
)
|
||||
}
|
||||
|
||||
close()
|
||||
}
|
||||
|
||||
async function createTeamspace (): Promise<void> {
|
||||
if (typeId === undefined || spaceType?.targetClass === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const teamspaceId = generateId<Teamspace>()
|
||||
const teamspaceData = getTeamspaceData()
|
||||
|
||||
await client.createDoc(document.class.Teamspace, core.space.Space, { ...teamspaceData }, teamspaceId)
|
||||
await client.createDoc(document.class.Teamspace, core.space.Space, { ...teamspaceData, type: typeId }, teamspaceId)
|
||||
|
||||
// Create space type's mixin with roles assignments
|
||||
await client.createMixin(
|
||||
teamspaceId,
|
||||
document.class.Teamspace,
|
||||
core.space.Space,
|
||||
spaceType.targetClass,
|
||||
rolesAssignment
|
||||
)
|
||||
|
||||
close(teamspaceId)
|
||||
}
|
||||
@ -141,13 +209,42 @@
|
||||
function close (id?: Ref<Teamspace>): void {
|
||||
dispatch('close', id)
|
||||
}
|
||||
|
||||
function handleTypeChange (evt: CustomEvent<Ref<SpaceType>>): void {
|
||||
typeId = evt.detail
|
||||
}
|
||||
|
||||
$: roles = (spaceType?.$lookup?.roles ?? []) as Role[]
|
||||
|
||||
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)
|
||||
const removedMembersSet = new Set(members.filter((m) => !newMembersSet.has(m)))
|
||||
|
||||
if (removedMembersSet.size > 0 && rolesAssignment !== undefined) {
|
||||
for (const [key, value] of Object.entries(rolesAssignment)) {
|
||||
rolesAssignment[key as Ref<Role>] =
|
||||
value !== undefined ? value.filter((m) => !removedMembersSet.has(m)) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
members = newMembers
|
||||
}
|
||||
|
||||
function handleRoleAssignmentChanged (roleId: Ref<Role>, newMembers: Ref<Account>[]): void {
|
||||
if (rolesAssignment === undefined) {
|
||||
rolesAssignment = {}
|
||||
}
|
||||
|
||||
rolesAssignment[roleId] = newMembers
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={isNew ? document.string.NewTeamspace : document.string.EditTeamspace}
|
||||
label={isNew ? documentRes.string.NewTeamspace : documentRes.string.EditTeamspace}
|
||||
okLabel={isNew ? presentation.string.Create : presentation.string.Save}
|
||||
okAction={handleSave}
|
||||
canSave={name.length > 0 && !(members.length === 0 && isPrivate)}
|
||||
canSave={name.length > 0 && !(members.length === 0 && isPrivate) && typeId !== undefined}
|
||||
accentHeader
|
||||
width={'medium'}
|
||||
gap={'gapV-6'}
|
||||
@ -157,12 +254,28 @@
|
||||
<div class="antiGrid">
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={document.string.TeamspaceTitle} />
|
||||
<Label label={core.string.SpaceType} />
|
||||
</div>
|
||||
|
||||
<SpaceTypeSelector
|
||||
disabled={!isNew}
|
||||
descriptors={[document.descriptor.TeamspaceType]}
|
||||
type={typeId}
|
||||
focusIndex={4}
|
||||
kind="regular"
|
||||
size="large"
|
||||
on:change={handleTypeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={documentRes.string.TeamspaceTitle} />
|
||||
</div>
|
||||
<div class="padding">
|
||||
<EditBox
|
||||
bind:value={name}
|
||||
placeholder={document.string.TeamspaceTitlePlaceholder}
|
||||
placeholder={documentRes.string.TeamspaceTitlePlaceholder}
|
||||
kind={'large-style'}
|
||||
autoFocus
|
||||
on:input={() => {
|
||||
@ -176,14 +289,14 @@
|
||||
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header topAlign">
|
||||
<Label label={document.string.Description} />
|
||||
<Label label={documentRes.string.Description} />
|
||||
</div>
|
||||
<div class="padding clear-mins">
|
||||
<StyledTextBox
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
bind:content={description}
|
||||
placeholder={document.string.TeamspaceDescriptionPlaceholder}
|
||||
placeholder={documentRes.string.TeamspaceDescriptionPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -192,7 +305,7 @@
|
||||
<div class="antiGrid">
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={document.string.ChooseIcon} />
|
||||
<Label label={documentRes.string.ChooseIcon} />
|
||||
</div>
|
||||
<Button
|
||||
icon={icon === view.ids.IconWithEmoji ? IconWithEmoji : icon ?? document.icon.Teamspace}
|
||||
@ -219,15 +332,34 @@
|
||||
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={document.string.TeamspaceMembers} />
|
||||
<Label label={documentRes.string.TeamspaceMembers} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
value={members}
|
||||
label={document.string.TeamspaceMembers}
|
||||
onChange={(refs) => (members = refs)}
|
||||
label={documentRes.string.TeamspaceMembers}
|
||||
onChange={handleMembersChanged}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#each roles as role}
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={documentRes.string.RoleLabel} params={{ role: role.name }} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
value={rolesAssignment?.[role._id] ?? []}
|
||||
label={documentRes.string.TeamspaceMembers}
|
||||
includeItems={members}
|
||||
readonly={members.length === 0}
|
||||
onChange={(refs) => {
|
||||
handleRoleAssignmentChanged(role._id, refs)
|
||||
}}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -19,7 +19,6 @@ import {
|
||||
type DocumentQuery,
|
||||
type Ref,
|
||||
type RelatedDocument,
|
||||
type Space,
|
||||
type WithLookup,
|
||||
generateId,
|
||||
getCurrentAccount
|
||||
@ -89,7 +88,7 @@ async function createDocument (space: Teamspace): Promise<void> {
|
||||
await _createDocument(id, space._id, parent)
|
||||
}
|
||||
|
||||
async function _createDocument (id: Ref<Document>, space: Ref<Space>, parent: Ref<Document>): Promise<void> {
|
||||
async function _createDocument (id: Ref<Document>, space: Ref<Teamspace>, parent: Ref<Document>): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
await createEmptyDocument(client, id, space, parent, {})
|
||||
|
@ -88,6 +88,8 @@ export default mergeIds(documentId, document, {
|
||||
ReassignToDoConfirm: '' as IntlString,
|
||||
|
||||
Color: '' as IntlString,
|
||||
Icon: '' as IntlString
|
||||
Icon: '' as IntlString,
|
||||
|
||||
RoleLabel: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import type { Class, Doc, Ref, Space, SpaceType, SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
import { NotificationGroup, NotificationType } from '@hcengineering/notification'
|
||||
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { IntlString, plugin } from '@hcengineering/platform'
|
||||
@ -77,6 +77,12 @@ export const documentPlugin = plugin(documentId, {
|
||||
},
|
||||
viewlet: {
|
||||
TableBranches: '' as Ref<Viewlet>
|
||||
},
|
||||
descriptor: {
|
||||
TeamspaceType: '' as Ref<SpaceTypeDescriptor>
|
||||
},
|
||||
spaceType: {
|
||||
DefaultTeamspaceType: '' as Ref<SpaceType>
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -14,12 +14,12 @@
|
||||
//
|
||||
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachedDoc, Class, CollaborativeDoc, Ref, Space } from '@hcengineering/core'
|
||||
import { AttachedDoc, Class, CollaborativeDoc, Ref, TypedSpace } from '@hcengineering/core'
|
||||
import { Preference } from '@hcengineering/preference'
|
||||
import { IconProps } from '@hcengineering/view'
|
||||
|
||||
/** @public */
|
||||
export interface Teamspace extends Space, IconProps {}
|
||||
export interface Teamspace extends TypedSpace, IconProps {}
|
||||
|
||||
/** @public */
|
||||
export interface Document extends AttachedDoc<Document, 'children', Teamspace>, IconProps {
|
||||
|
@ -32,6 +32,8 @@
|
||||
"UnAssign": "Unassign",
|
||||
"ConfigLabel": "CRM",
|
||||
"ConfigDescription": "Extension for Customer relation management",
|
||||
"EditFunnel": "Edit Funnel"
|
||||
"EditFunnel": "Edit Funnel",
|
||||
"FunnelMembers": "Members",
|
||||
"RoleLabel": "Role: {role}"
|
||||
}
|
||||
}
|
@ -32,6 +32,8 @@
|
||||
"UnAssign": "Отменить назначение",
|
||||
"ConfigLabel": "CRM",
|
||||
"ConfigDescription": "Расширение по работе с клиентами",
|
||||
"EditFunnel": "Редактировать воронку"
|
||||
"EditFunnel": "Редактировать воронку",
|
||||
"FunnelMembers": "Участники",
|
||||
"RoleLabel": "Роль: {role}"
|
||||
}
|
||||
}
|
@ -56,6 +56,7 @@
|
||||
"@hcengineering/view": "^0.6.9",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
"@hcengineering/workbench": "^0.6.9",
|
||||
"svelte": "^4.2.12"
|
||||
"svelte": "^4.2.12",
|
||||
"fast-equals": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -14,14 +14,25 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { AccountArrayEditor } from '@hcengineering/contact-resources'
|
||||
import core, { Account, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { Funnel } from '@hcengineering/lead'
|
||||
import presentation, { getClient, SpaceCreateCard } from '@hcengineering/presentation'
|
||||
import core, {
|
||||
Account,
|
||||
getCurrentAccount,
|
||||
Ref,
|
||||
Role,
|
||||
RolesAssignment,
|
||||
SortingOrder,
|
||||
SpaceType,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import lead, { Funnel } from '@hcengineering/lead'
|
||||
import presentation, { createQuery, getClient, SpaceCreateCard } from '@hcengineering/presentation'
|
||||
import task, { ProjectType } from '@hcengineering/task'
|
||||
import ui, { Component, EditBox, Grid, Label, ToggleWithLabel } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import lead from '../plugin'
|
||||
|
||||
import leadRes from '../plugin'
|
||||
|
||||
export let funnel: Funnel | undefined = undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -33,38 +44,112 @@
|
||||
|
||||
let name: string = funnel?.name ?? ''
|
||||
const description: string = funnel?.description ?? ''
|
||||
let type: Ref<ProjectType> | undefined
|
||||
let typeId: Ref<ProjectType> | undefined = funnel?.type ?? lead.template.DefaultFunnel
|
||||
let spaceType: WithLookup<SpaceType> | undefined
|
||||
let rolesAssignment: RolesAssignment = {}
|
||||
let isPrivate: boolean = funnel?.private ?? false
|
||||
|
||||
let members: Ref<Account>[] =
|
||||
funnel?.members !== undefined ? hierarchy.clone(funnel.members) : [getCurrentAccount()._id]
|
||||
|
||||
$: void loadSpaceType(typeId)
|
||||
async function loadSpaceType (id: typeof typeId): Promise<void> {
|
||||
spaceType =
|
||||
id !== undefined
|
||||
? await client
|
||||
.getModel()
|
||||
.findOne(core.class.SpaceType, { _id: id }, { lookup: { _id: { roles: core.class.Role } } })
|
||||
: undefined
|
||||
|
||||
if (spaceType?.targetClass === undefined || spaceType?.$lookup?.roles === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
rolesAssignment = getRolesAssignment()
|
||||
}
|
||||
|
||||
$: roles = (spaceType?.$lookup?.roles ?? []) as Role[]
|
||||
|
||||
function getRolesAssignment (): RolesAssignment {
|
||||
if (funnel === undefined || spaceType?.targetClass === undefined || spaceType?.$lookup?.roles === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const asMixin = hierarchy.as(funnel, spaceType?.targetClass)
|
||||
|
||||
return spaceType.$lookup.roles.reduce<RolesAssignment>((prev, { _id }) => {
|
||||
prev[_id as Ref<Role>] = (asMixin as any)[_id]
|
||||
|
||||
return prev
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function canClose (): boolean {
|
||||
return name === '' && type !== undefined
|
||||
return name === '' && typeId !== undefined
|
||||
}
|
||||
|
||||
async function createFunnel (): Promise<void> {
|
||||
if (type === undefined) return
|
||||
await client.createDoc(lead.class.Funnel, core.space.Space, {
|
||||
if (typeId === undefined || spaceType?.targetClass === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const funnelId = await client.createDoc(leadRes.class.Funnel, core.space.Space, {
|
||||
name,
|
||||
description,
|
||||
private: isPrivate,
|
||||
archived: false,
|
||||
members,
|
||||
type
|
||||
type: typeId
|
||||
})
|
||||
|
||||
// Create space type's mixin with roles assignments
|
||||
await client.createMixin(funnelId, leadRes.class.Funnel, core.space.Space, spaceType.targetClass, rolesAssignment)
|
||||
}
|
||||
|
||||
async function save (): Promise<void> {
|
||||
if (isNew) {
|
||||
await createFunnel()
|
||||
} else if (funnel !== undefined) {
|
||||
} else if (funnel !== undefined && spaceType?.targetClass !== undefined) {
|
||||
await client.diffUpdate<Funnel>(funnel, { name, description, members, private: isPrivate }, Date.now())
|
||||
|
||||
if (!deepEqual(rolesAssignment, getRolesAssignment())) {
|
||||
await client.updateMixin(
|
||||
funnel._id,
|
||||
leadRes.class.Funnel,
|
||||
core.space.Space,
|
||||
spaceType.targetClass,
|
||||
rolesAssignment
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
const removedMembersSet = new Set(members.filter((m) => !newMembersSet.has(m)))
|
||||
|
||||
if (removedMembersSet.size > 0 && rolesAssignment !== undefined) {
|
||||
for (const [key, value] of Object.entries(rolesAssignment)) {
|
||||
rolesAssignment[key as Ref<Role>] =
|
||||
value !== undefined ? value.filter((m) => !removedMembersSet.has(m)) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
members = newMembers
|
||||
}
|
||||
|
||||
function handleRoleAssignmentChanged (roleId: Ref<Role>, newMembers: Ref<Account>[]): void {
|
||||
if (rolesAssignment === undefined) {
|
||||
rolesAssignment = {}
|
||||
}
|
||||
|
||||
rolesAssignment[roleId] = newMembers
|
||||
}
|
||||
</script>
|
||||
|
||||
<SpaceCreateCard
|
||||
label={lead.string.CreateFunnel}
|
||||
label={leadRes.string.CreateFunnel}
|
||||
okAction={save}
|
||||
okLabel={!isNew ? ui.string.Save : undefined}
|
||||
canSave={name.length > 0}
|
||||
@ -73,7 +158,7 @@
|
||||
}}
|
||||
>
|
||||
<Grid column={1} rowGap={1.5}>
|
||||
<EditBox label={lead.string.FunnelName} bind:value={name} placeholder={lead.string.FunnelName} autoFocus />
|
||||
<EditBox label={leadRes.string.FunnelName} bind:value={name} placeholder={leadRes.string.FunnelName} autoFocus />
|
||||
<ToggleWithLabel
|
||||
label={presentation.string.MakePrivate}
|
||||
description={presentation.string.MakePrivateDescription}
|
||||
@ -82,26 +167,46 @@
|
||||
|
||||
<Component
|
||||
is={task.component.ProjectTypeSelector}
|
||||
disabled={!isNew}
|
||||
props={{
|
||||
categories: [lead.descriptors.FunnelType],
|
||||
type,
|
||||
descriptors: [leadRes.descriptors.FunnelType],
|
||||
type: typeId,
|
||||
disabled: funnel !== undefined
|
||||
}}
|
||||
on:change={(evt) => {
|
||||
type = evt.detail
|
||||
typeId = evt.detail
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<div class="antiGrid-row mt-4">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={leadRes.string.Members} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
value={members}
|
||||
label={leadRes.string.Members}
|
||||
onChange={handleMembersChanged}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#each roles as role}
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={lead.string.Members} />
|
||||
<Label label={leadRes.string.RoleLabel} params={{ role: role.name }} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
value={members}
|
||||
label={lead.string.Members}
|
||||
onChange={(refs) => (members = refs)}
|
||||
value={rolesAssignment?.[role._id] ?? []}
|
||||
label={leadRes.string.FunnelMembers}
|
||||
includeItems={members}
|
||||
readonly={members.length === 0}
|
||||
onChange={(refs) => {
|
||||
handleRoleAssignmentChanged(role._id, refs)
|
||||
}}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
{/each}
|
||||
</SpaceCreateCard>
|
||||
|
@ -42,7 +42,9 @@ export default mergeIds(leadId, lead, {
|
||||
FunnelPlaceholder: '' as IntlString,
|
||||
Members: '' as IntlString,
|
||||
Assignee: '' as IntlString,
|
||||
UnAssign: '' as IntlString
|
||||
UnAssign: '' as IntlString,
|
||||
FunnelMembers: '' as IntlString,
|
||||
RoleLabel: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
CreateCustomer: '' as AnyComponent,
|
||||
|
@ -19,7 +19,7 @@ import type { Attribute, Class, Doc, Ref, Status, Timestamp } from '@hcengineeri
|
||||
import { Mixin } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { Project, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task'
|
||||
import type { Project, ProjectType, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -87,6 +87,9 @@ const lead = plugin(leadId, {
|
||||
},
|
||||
taskType: {
|
||||
Lead: '' as Ref<TaskType>
|
||||
},
|
||||
template: {
|
||||
DefaultFunnel: '' as Ref<ProjectType>
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -15,8 +15,19 @@
|
||||
<script lang="ts">
|
||||
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||
import contact, { Organization } from '@hcengineering/contact'
|
||||
import { UserBox } from '@hcengineering/contact-resources'
|
||||
import core, { Data, fillDefaults, FindResult, generateId, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { AccountArrayEditor, UserBox } from '@hcengineering/contact-resources'
|
||||
import core, {
|
||||
Account,
|
||||
Class,
|
||||
Data,
|
||||
fillDefaults,
|
||||
FindResult,
|
||||
generateId,
|
||||
Ref,
|
||||
Role,
|
||||
RolesAssignment,
|
||||
SortingOrder
|
||||
} from '@hcengineering/core'
|
||||
import { Card, createQuery, getClient, InlineAttributeBar, MessageBox } from '@hcengineering/presentation'
|
||||
import { Vacancy as VacancyClass } from '@hcengineering/recruit'
|
||||
import tags from '@hcengineering/tags'
|
||||
@ -36,6 +47,7 @@
|
||||
import Company from './icons/Company.svelte'
|
||||
import Vacancy from './icons/Vacancy.svelte'
|
||||
import { selectedTypeStore, typeStore } from '@hcengineering/task-resources'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -48,7 +60,6 @@
|
||||
let appliedTemplateId: Ref<ProjectType> | undefined
|
||||
let objectId: Ref<VacancyClass> = generateId()
|
||||
let issueTemplates: FindResult<IssueTemplate> = []
|
||||
|
||||
let fullDescription: string = ''
|
||||
|
||||
export let company: Ref<Organization> | undefined
|
||||
@ -78,7 +89,7 @@
|
||||
fillDefaults(hierarchy, vacancyData, recruit.class.Vacancy)
|
||||
$: typeId &&
|
||||
templateQ.query(task.class.ProjectType, { _id: typeId }, (result) => {
|
||||
const { _class, _id, description, ...templateData } = result[0]
|
||||
const { _class, _id, description, targetClass, ...templateData } = result[0]
|
||||
vacancyData = { ...(templateData as unknown as Data<VacancyClass>), fullDescription: description }
|
||||
if (appliedTemplateId !== typeId) {
|
||||
fullDescription = description ?? ''
|
||||
@ -92,6 +103,34 @@
|
||||
issueTemplates = result
|
||||
})
|
||||
|
||||
let rolesAssignment: RolesAssignment | undefined
|
||||
let roles: Role[] = []
|
||||
const rolesQuery = createQuery()
|
||||
$: if (typeType !== undefined) {
|
||||
rolesQuery.query(
|
||||
core.class.Role,
|
||||
{ attachedTo: typeType._id },
|
||||
(res) => {
|
||||
roles = res
|
||||
|
||||
if (rolesAssignment === undefined) {
|
||||
rolesAssignment = roles.reduce<RolesAssignment>((prev, { _id }) => {
|
||||
prev[_id] = []
|
||||
|
||||
return prev
|
||||
}, {})
|
||||
}
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
name: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
rolesQuery.unsubscribe()
|
||||
}
|
||||
|
||||
async function saveIssue (
|
||||
id: Ref<VacancyClass>,
|
||||
space: Ref<Project>,
|
||||
@ -196,8 +235,14 @@
|
||||
|
||||
await descriptionBox.createAttachments()
|
||||
|
||||
// Add vacancy mixin
|
||||
await client.createMixin(objectId, recruit.class.Vacancy, core.space.Space, typeType.targetClass, {})
|
||||
// Add vacancy mixin with roles assignment
|
||||
await client.createMixin(
|
||||
objectId,
|
||||
recruit.class.Vacancy,
|
||||
core.space.Space,
|
||||
typeType.targetClass,
|
||||
rolesAssignment ?? {}
|
||||
)
|
||||
|
||||
objectId = generateId()
|
||||
|
||||
@ -228,6 +273,14 @@
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleRoleAssignmentChanged (roleId: Ref<Role>, newMembers: Ref<Account>[]): void {
|
||||
if (rolesAssignment === undefined) {
|
||||
rolesAssignment = {}
|
||||
}
|
||||
|
||||
rolesAssignment[roleId] = newMembers
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -304,9 +357,22 @@
|
||||
_class={recruit.class.Vacancy}
|
||||
object={vacancyData}
|
||||
toClass={core.class.Space}
|
||||
ignoreKeys={['fullDescription', 'company']}
|
||||
ignoreKeys={['fullDescription', 'company', 'type']}
|
||||
extraProps={{ showNavigate: false }}
|
||||
/>
|
||||
|
||||
{#each roles as role}
|
||||
<AccountArrayEditor
|
||||
value={rolesAssignment?.[role._id] ?? []}
|
||||
label={getEmbeddedLabel(role.name)}
|
||||
emptyLabel={getEmbeddedLabel(role.name)}
|
||||
onChange={(refs) => {
|
||||
handleRoleAssignmentChanged(role._id, refs)
|
||||
}}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
|
@ -137,7 +137,7 @@
|
||||
|
||||
<svelte:fragment slot="attributes" let:direction={dir}>
|
||||
{#if dir === 'column'}
|
||||
<DocAttributeBar {object} {mixins} ignoreKeys={['name', 'fullDescription', 'private', 'archived']} />
|
||||
<DocAttributeBar {object} {mixins} ignoreKeys={['name', 'fullDescription', 'private', 'archived', 'type']} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
|
@ -100,6 +100,15 @@
|
||||
"Automations": "Automations",
|
||||
"Collections": "Collections",
|
||||
"ClassColon": "Class:",
|
||||
"Branding": "Branding"
|
||||
"Branding": "Branding",
|
||||
"SpaceTypes": "Space types",
|
||||
"NewSpaceType": "New space type",
|
||||
"SpaceTypeTitle": "Space type title",
|
||||
"General": "General",
|
||||
"Description": "Description",
|
||||
"CountSpaces": "{count, plural, =0 {No spaces} =1 {# space} other {# spaces}}",
|
||||
"Roles": "Roles",
|
||||
"RoleName": "Role name",
|
||||
"Permissions": "Permissions"
|
||||
}
|
||||
}
|
@ -101,6 +101,15 @@
|
||||
"Automations": "Автоматизация",
|
||||
"Collections": "Коллекции",
|
||||
"ClassColon": "Класс:",
|
||||
"Branding": "Брендинг"
|
||||
"Branding": "Брендинг",
|
||||
"SpaceTypes": "Типы пространств",
|
||||
"NewSpaceType": "Новый тип пространства",
|
||||
"SpaceTypeTitle": "Название типа пространства",
|
||||
"General": "Общее",
|
||||
"Description": "Описание",
|
||||
"CountSpaces": "{count, plural, =0 {Нет пространств} =1 {# пространство} =2 {# пространства} =3 {# пространства} =4 {# пространства} other {# пространств}}",
|
||||
"Roles": "Роли",
|
||||
"RoleName": "Название роли",
|
||||
"Permissions": "Разрешения"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M10.2857 5.44444C10.2857 6.79447 9.26237 7.88889 8 7.88889C6.73764 7.88889 5.71429 6.79447 5.71429 5.44444C5.71429 4.09441 6.73764 3 8 3C9.26237 3 10.2857 4.09441 10.2857 5.44444Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.42857 11.9603C3.42857 10.9748 3.98128 10.081 4.85831 9.7786C5.79546 9.45545 7.02325 9.11111 8 9.11111C8.97675 9.11111 10.2045 9.45545 11.1417 9.77859C12.0187 10.081 12.5714 10.9748 12.5714 11.9603V12.7778C12.5714 13.4528 12.0598 14 11.4286 14H4.57143C3.94025 14 3.42857 13.4528 3.42857 12.7778V11.9603Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.63272 4.75066C4.59249 4.97531 4.57143 5.20725 4.57143 5.44444C4.57143 6.00206 4.68782 6.5306 4.89604 7.00381C4.59565 7.53433 4.05083 7.88889 3.42857 7.88889C2.4818 7.88889 1.71429 7.06808 1.71429 6.05556C1.71429 5.04303 2.4818 4.22222 3.42857 4.22222C3.89788 4.22222 4.32315 4.42391 4.63272 4.75066Z"
|
||||
/>
|
||||
<path
|
||||
d="M2.28571 12.7778V11.9603C2.28571 10.8522 2.76028 9.77952 3.59669 9.11537C3.5397 9.11257 3.48361 9.11111 3.42857 9.11111C2.69601 9.11111 1.77516 9.36937 1.0723 9.61172C0.414531 9.83853 0 10.5089 0 11.248V11.8611C0 12.3674 0.383756 12.7778 0.857143 12.7778H2.28571Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.3716 4.75066C11.4118 4.97531 11.4329 5.20725 11.4329 5.44444C11.4329 6.00206 11.3166 6.5306 11.1086 7.00381C11.4087 7.53433 11.953 7.88889 12.5747 7.88889C13.5205 7.88889 14.2873 7.06808 14.2873 6.05556C14.2873 5.04303 13.5205 4.22222 12.5747 4.22222C12.1058 4.22222 11.6809 4.42391 11.3716 4.75066Z"
|
||||
/>
|
||||
<path
|
||||
d="M13.7164 12.7778V11.9603C13.7164 10.8522 13.2423 9.77952 12.4067 9.11537C12.4636 9.11257 12.5197 9.11111 12.5747 9.11111C13.3065 9.11111 14.2265 9.36937 14.9287 9.61172C15.5859 9.83853 16 10.5089 16 11.248V11.8611C16 12.3674 15.6166 12.7778 15.1437 12.7778H13.7164Z"
|
||||
/>
|
||||
</svg>
|
28
plugins/setting-resources/src/components/icons/Person.svelte
Normal file
28
plugins/setting-resources/src/components/icons/Person.svelte
Normal file
@ -0,0 +1,28 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 3.99988C7.50555 3.99988 7.0222 4.1465 6.61108 4.4212C6.19995 4.69591 5.87952 5.08635 5.6903 5.54317C5.50108 5.99998 5.45158 6.50265 5.54804 6.9876C5.6445 7.47256 5.88261 7.91801 6.23224 8.26764C6.58187 8.61728 7.02733 8.85538 7.51228 8.95184C7.99723 9.0483 8.4999 8.9988 8.95671 8.80958C9.41353 8.62036 9.80397 8.29993 10.0787 7.8888C10.3534 7.47768 10.5 6.99433 10.5 6.49988C10.5 5.83684 10.2366 5.20095 9.76777 4.73211C9.29893 4.26327 8.66304 3.99988 8 3.99988ZM8 7.99988C7.70333 7.99988 7.41332 7.9119 7.16665 7.74708C6.91997 7.58226 6.72771 7.34799 6.61418 7.0739C6.50065 6.79981 6.47095 6.49821 6.52882 6.20724C6.5867 5.91627 6.72956 5.649 6.93934 5.43922C7.14912 5.22944 7.4164 5.08658 7.70737 5.0287C7.99834 4.97082 8.29994 5.00053 8.57403 5.11406C8.84812 5.22759 9.08239 5.41985 9.24721 5.66652C9.41203 5.9132 9.5 6.20321 9.5 6.49988C9.49955 6.89756 9.34137 7.27883 9.06017 7.56004C8.77896 7.84125 8.39769 7.99943 8 7.99988Z"
|
||||
/>
|
||||
<path
|
||||
d="M8 0.999878C6.61553 0.999878 5.26216 1.41042 4.11101 2.17959C2.95987 2.94876 2.06266 4.04201 1.53285 5.32109C1.00303 6.60018 0.86441 8.00764 1.13451 9.36551C1.4046 10.7234 2.07129 11.9707 3.05026 12.9496C4.02922 13.9286 5.2765 14.5953 6.63437 14.8654C7.99224 15.1355 9.3997 14.9968 10.6788 14.467C11.9579 13.9372 13.0511 13.04 13.8203 11.8889C14.5895 10.7377 15 9.38435 15 7.99988C14.9979 6.144 14.2597 4.36474 12.9474 3.05244C11.6351 1.74014 9.85588 1.00197 8 0.999878ZM5 13.1881V12.4999C5.00044 12.1022 5.15862 11.7209 5.43983 11.4397C5.72104 11.1585 6.10231 11.0003 6.5 10.9999H9.5C9.89769 11.0003 10.279 11.1585 10.5602 11.4397C10.8414 11.7209 10.9996 12.1022 11 12.4999V13.1881C10.0896 13.7197 9.05426 13.9999 8 13.9999C6.94574 13.9999 5.91042 13.7197 5 13.1881ZM11.9963 12.4628C11.9863 11.8069 11.7191 11.1812 11.2521 10.7205C10.7852 10.2598 10.156 10.001 9.5 9.99988H6.5C5.84405 10.001 5.2148 10.2598 4.74786 10.7205C4.28093 11.1812 4.01369 11.8069 4.00375 12.4628C3.09703 11.6532 2.45762 10.5872 2.17017 9.40611C1.88272 8.22501 1.9608 6.98445 2.39407 5.84871C2.82734 4.71297 3.59536 3.73561 4.59644 3.04606C5.59751 2.35651 6.78442 1.98729 8 1.98729C9.21558 1.98729 10.4025 2.35651 11.4036 3.04606C12.4046 3.73561 13.1727 4.71297 13.6059 5.84871C14.0392 6.98445 14.1173 8.22501 13.8298 9.40611C13.5424 10.5872 12.903 11.6532 11.9963 12.4628Z"
|
||||
/>
|
||||
</svg>
|
@ -0,0 +1,28 @@
|
||||
<!--
|
||||
|
||||
// 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 { Permission } from '@hcengineering/core'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
|
||||
export let value: Permission
|
||||
export let inline: boolean = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="flex-presenter" class:inline-presenter={inline}>
|
||||
<Label label={value.label} />
|
||||
</div>
|
||||
{/if}
|
@ -0,0 +1,28 @@
|
||||
<!--
|
||||
|
||||
// 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 { SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
|
||||
export let value: SpaceTypeDescriptor
|
||||
export let inline: boolean = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="flex-presenter" class:inline-presenter={inline}>
|
||||
<Label label={value.name} />
|
||||
</div>
|
||||
{/if}
|
@ -0,0 +1,127 @@
|
||||
<!--
|
||||
// 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 core, { Class, Ref, SpaceTypeDescriptor, generateId, SpaceType, Data } from '@hcengineering/core'
|
||||
import { Card, getClient, hasResource } from '@hcengineering/presentation'
|
||||
import { AnySvelteComponent, EditBox } from '@hcengineering/ui'
|
||||
import { Resource, getResource } from '@hcengineering/platform'
|
||||
import { ObjectBox } from '@hcengineering/view-resources'
|
||||
import setting, { SpaceTypeCreator, createSpaceType } from '@hcengineering/setting'
|
||||
|
||||
import settingRes from '../../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let name: string = ''
|
||||
let descriptor: SpaceTypeDescriptor | undefined = undefined
|
||||
let handleTypeCreated: (() => Promise<void>) | undefined
|
||||
|
||||
async function createType (): Promise<void> {
|
||||
if (descriptor === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (handleTypeCreated !== undefined) {
|
||||
await handleTypeCreated()
|
||||
} else {
|
||||
const data: Omit<Data<SpaceType>, 'targetClass'> = {
|
||||
name,
|
||||
descriptor: descriptor._id,
|
||||
roles: 0
|
||||
}
|
||||
|
||||
await createSpaceType(client, data, generateId())
|
||||
}
|
||||
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
const descriptors = client
|
||||
.getModel()
|
||||
.findAllSync(core.class.SpaceTypeDescriptor, {})
|
||||
.filter((descriptor) => hasResource(descriptor._id as any as Resource<any>))
|
||||
|
||||
descriptor = descriptors[0]
|
||||
|
||||
$: typeCreator =
|
||||
descriptor !== undefined
|
||||
? hierarchy.classHierarchyMixin<Class<SpaceTypeDescriptor>, SpaceTypeCreator>(
|
||||
descriptor._class,
|
||||
setting.mixin.SpaceTypeCreator
|
||||
)
|
||||
: undefined
|
||||
|
||||
let extraComponent: AnySvelteComponent | undefined
|
||||
$: loadExtraComponent(typeCreator)
|
||||
|
||||
async function loadExtraComponent (tc: SpaceTypeCreator | undefined): Promise<void> {
|
||||
if (tc === undefined) {
|
||||
extraComponent = undefined
|
||||
handleTypeCreated = undefined
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
extraComponent = await getResource(tc.extraComponent)
|
||||
}
|
||||
|
||||
function handleDescriptorSelected (evt: CustomEvent<Ref<SpaceTypeDescriptor>>): void {
|
||||
descriptor = descriptors.find((it) => it._id === evt.detail)
|
||||
}
|
||||
|
||||
$: canSave = name.trim().length > 0 && descriptor !== undefined
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={settingRes.string.NewSpaceType}
|
||||
{canSave}
|
||||
okAction={createType}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
on:changeContent
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<ObjectBox
|
||||
_class={core.class.SpaceTypeDescriptor}
|
||||
value={descriptor?._id}
|
||||
on:change={handleDescriptorSelected}
|
||||
kind="regular"
|
||||
size="small"
|
||||
label={core.string.SpaceType}
|
||||
searchField="name"
|
||||
showNavigate={false}
|
||||
focusIndex={20000}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<div class="flex-col flex-gap-2">
|
||||
<EditBox
|
||||
bind:value={name}
|
||||
placeholder={core.string.SpaceType}
|
||||
kind="large-style"
|
||||
focusIndex={1}
|
||||
autoFocus
|
||||
fullSize
|
||||
/>
|
||||
|
||||
{#if extraComponent !== undefined}
|
||||
<svelte:component this={extraComponent} {name} {descriptor} bind:handleTypeCreated />
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
@ -0,0 +1,187 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022, 2023 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, onDestroy } from 'svelte'
|
||||
import core, { Class, Doc, IdMap, Ref, SpaceType, WithLookup, toIdMap } from '@hcengineering/core'
|
||||
import {
|
||||
Location,
|
||||
resolvedLocationStore,
|
||||
resizeObserver,
|
||||
Breadcrumbs,
|
||||
ButtonIcon,
|
||||
Header,
|
||||
IconCopy,
|
||||
IconDelete,
|
||||
IconMoreV,
|
||||
AnySvelteComponent,
|
||||
navigate,
|
||||
getCurrentResolvedLocation
|
||||
} from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { showMenu } from '@hcengineering/view-resources'
|
||||
import setting, { SpaceTypeEditor } from '@hcengineering/setting'
|
||||
import { Asset, getResource } from '@hcengineering/platform'
|
||||
|
||||
import SpaceTypeEditorComponent from './editor/SpaceTypeEditor.svelte'
|
||||
import { clearSettingsStore } from '../../store'
|
||||
|
||||
export let visibleNav: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let visibleSecondNav: boolean = true
|
||||
let type: WithLookup<SpaceType> | undefined
|
||||
let selectedTypeId: Ref<SpaceType> | undefined
|
||||
let typesMap: IdMap<SpaceType> | undefined
|
||||
let selectedSubEditorId: string | undefined
|
||||
let selectedSubObjectId: Ref<Doc> | undefined
|
||||
let subItemName: string | undefined
|
||||
let subItemIcon: Asset | undefined
|
||||
|
||||
onDestroy(resolvedLocationStore.subscribe(handleLocationChanged))
|
||||
|
||||
function handleLocationChanged ({ path }: Location): void {
|
||||
selectedTypeId = path[4] as Ref<SpaceType>
|
||||
|
||||
if (path.length === 7) {
|
||||
selectSubItem(path[5], path[6] as Ref<Doc>)
|
||||
} else {
|
||||
selectSubItem(undefined, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function selectSubItem (editorId: string | undefined, objId: Ref<Doc> | undefined): void {
|
||||
selectedSubEditorId = editorId
|
||||
selectedSubObjectId = objId
|
||||
}
|
||||
|
||||
const typesQuery = createQuery()
|
||||
typesQuery.query(
|
||||
core.class.SpaceType,
|
||||
{},
|
||||
(res) => {
|
||||
typesMap = toIdMap(res)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
descriptor: core.class.SpaceTypeDescriptor
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$: type = selectedTypeId !== undefined && typesMap !== undefined ? typesMap.get(selectedTypeId) : undefined
|
||||
$: descriptor = type?.$lookup?.descriptor
|
||||
$: editorDescriptor =
|
||||
type !== undefined
|
||||
? hierarchy.classHierarchyMixin<Class<SpaceType>, SpaceTypeEditor>(type._class, setting.mixin.SpaceTypeEditor)
|
||||
: undefined
|
||||
|
||||
$: subEditorRes =
|
||||
selectedSubEditorId !== undefined && editorDescriptor !== undefined
|
||||
? editorDescriptor?.subEditors?.[selectedSubEditorId]
|
||||
: undefined
|
||||
let subEditor: AnySvelteComponent | undefined
|
||||
$: if (subEditorRes !== undefined) {
|
||||
void getResource(subEditorRes).then((res) => (subEditor = res))
|
||||
} else {
|
||||
subEditor = undefined
|
||||
}
|
||||
|
||||
let bcItems: Array<{ title: string, icon?: Asset }> = []
|
||||
$: {
|
||||
bcItems = []
|
||||
|
||||
if (type !== undefined) {
|
||||
bcItems.push({ title: type.name, icon: descriptor?.icon })
|
||||
|
||||
if (selectedSubObjectId) {
|
||||
bcItems.push({ title: subItemName ?? selectedSubObjectId, icon: subItemIcon })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCrumbSelected (event: CustomEvent): void {
|
||||
if (event.detail === 0) {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.path.length = 5
|
||||
|
||||
clearSettingsStore()
|
||||
navigate(loc)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="hulyComponent"
|
||||
use:resizeObserver={(element) => {
|
||||
visibleSecondNav = element.clientWidth > 720
|
||||
}}
|
||||
>
|
||||
{#if type !== undefined && descriptor !== undefined}
|
||||
<Header minimize={!visibleNav} on:resize={(event) => dispatch('change', event.detail)}>
|
||||
<ButtonIcon
|
||||
icon={IconCopy}
|
||||
size={'small'}
|
||||
kind={'secondary'}
|
||||
disabled
|
||||
on:click={(ev) => {
|
||||
// TODO: copy space type
|
||||
}}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
size={'small'}
|
||||
kind={'secondary'}
|
||||
disabled
|
||||
on:click={(ev) => {
|
||||
// TODO: delete space type
|
||||
}}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={IconMoreV}
|
||||
size={'small'}
|
||||
kind={'secondary'}
|
||||
on:click={(ev) => {
|
||||
showMenu(ev, { object: type })
|
||||
}}
|
||||
/>
|
||||
<Breadcrumbs
|
||||
items={bcItems}
|
||||
size="large"
|
||||
selected={selectedSubObjectId ? 1 : 0}
|
||||
on:select={handleCrumbSelected}
|
||||
/>
|
||||
</Header>
|
||||
{#if editorDescriptor !== undefined}
|
||||
{#if subEditor === undefined}
|
||||
{#key type._id}
|
||||
<SpaceTypeEditorComponent {type} {descriptor} {editorDescriptor} {visibleSecondNav} on:change />
|
||||
{/key}
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={subEditor}
|
||||
bind:name={subItemName}
|
||||
bind:icon={subItemIcon}
|
||||
spaceType={type}
|
||||
{descriptor}
|
||||
objectId={selectedSubObjectId}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
@ -14,27 +14,38 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IdMap, Ref, WithLookup, toIdMap } from '@hcengineering/core'
|
||||
import task, { ProjectType } from '@hcengineering/task'
|
||||
import { Location, getCurrentResolvedLocation, navigate, resolvedLocationStore } from '@hcengineering/ui'
|
||||
|
||||
import { createQuery, hasResource } from '@hcengineering/presentation'
|
||||
import { onDestroy } from 'svelte'
|
||||
import Types from './Types.svelte'
|
||||
import core, { Ref, SpaceType, WithLookup } from '@hcengineering/core'
|
||||
import { Location, getCurrentResolvedLocation, navigate, resolvedLocationStore } from '@hcengineering/ui'
|
||||
import { createQuery, hasResource } from '@hcengineering/presentation'
|
||||
import { Resource } from '@hcengineering/platform'
|
||||
import { clearSettingsStore } from '@hcengineering/setting-resources'
|
||||
|
||||
export let kind: 'navigation' | 'tools' | undefined
|
||||
import { clearSettingsStore } from '../../store'
|
||||
import SpaceTypes from './SpaceTypes.svelte'
|
||||
|
||||
export let categoryName: string
|
||||
let type: WithLookup<ProjectType> | undefined
|
||||
let typeId: Ref<ProjectType> | undefined
|
||||
|
||||
onDestroy(
|
||||
resolvedLocationStore.subscribe((loc) => {
|
||||
void (async (loc: Location): Promise<void> => {
|
||||
typeId = loc.path[4] as Ref<ProjectType>
|
||||
})(loc)
|
||||
})
|
||||
let selectedTypeId: Ref<WithLookup<SpaceType>> | undefined
|
||||
|
||||
onDestroy(resolvedLocationStore.subscribe(handleLocationChanged))
|
||||
|
||||
function handleLocationChanged (loc: Location): void {
|
||||
selectedTypeId = loc.path[4] as Ref<SpaceType>
|
||||
}
|
||||
|
||||
let types: WithLookup<SpaceType>[] = []
|
||||
const typesQuery = createQuery()
|
||||
$: typesQuery.query(
|
||||
core.class.SpaceType,
|
||||
{},
|
||||
(result) => {
|
||||
types = result.filter((p) => hasResource(p.descriptor as any as Resource<any>))
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
descriptor: core.class.SpaceTypeDescriptor
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function selectProjectType (id: string): void {
|
||||
@ -46,31 +57,9 @@
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
let types: WithLookup<ProjectType>[] = []
|
||||
let typeMap: IdMap<WithLookup<ProjectType>> = new Map()
|
||||
const query = createQuery()
|
||||
$: query.query(
|
||||
task.class.ProjectType,
|
||||
{ archived: false },
|
||||
(result) => {
|
||||
types = result.filter((p) => hasResource(p.descriptor as any as Resource<any>))
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
descriptor: task.class.ProjectTypeDescriptor
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$: typeMap = toIdMap(types)
|
||||
$: type = typeId !== undefined ? typeMap.get(typeId) : undefined
|
||||
function handleTypeChange (event: CustomEvent): void {
|
||||
selectProjectType(event.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Types
|
||||
{type}
|
||||
{typeId}
|
||||
{types}
|
||||
on:change={(evt) => {
|
||||
selectProjectType(evt.detail)
|
||||
}}
|
||||
/>
|
||||
<SpaceTypes {selectedTypeId} {types} on:change={handleTypeChange} />
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022, 2023 Hardcore Engineering Inc.
|
||||
// Copyright © 2021, 2022, 2023, 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
|
||||
@ -15,11 +15,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button, IconAdd, showPopup } from '@hcengineering/ui'
|
||||
import CreateProjectType from './CreateProjectType.svelte'
|
||||
import CreateSpaceType from './CreateSpaceType.svelte'
|
||||
|
||||
function open () {
|
||||
showPopup(CreateProjectType, {}, 'top')
|
||||
function handleAdd (): void {
|
||||
showPopup(CreateSpaceType, {}, 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button id="new-project-type" icon={IconAdd} kind={'link'} size="small" on:click={open} />
|
||||
<Button id="new-space-type" icon={IconAdd} kind="link" size="small" on:click={handleAdd} />
|
@ -0,0 +1,130 @@
|
||||
<!--
|
||||
// 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 { AttributeEditor, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import core, { Permission, Ref, Role, SpaceType, SpaceTypeDescriptor, WithLookup } from '@hcengineering/core'
|
||||
import { ButtonIcon, Icon, IconEdit, IconSettings, Label, Scroller, showPopup } from '@hcengineering/ui'
|
||||
import { ObjectBoxPopup } from '@hcengineering/view-resources'
|
||||
|
||||
import PersonIcon from '../icons/Person.svelte'
|
||||
import settingRes from '../../plugin'
|
||||
|
||||
export let spaceType: SpaceType
|
||||
export let descriptor: SpaceTypeDescriptor
|
||||
export let objectId: Ref<Role>
|
||||
export let name: string | undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let role: Role | undefined
|
||||
const roleQuery = createQuery()
|
||||
$: roleQuery.query(core.class.Role, { _id: objectId }, (res) => {
|
||||
;[role] = res
|
||||
})
|
||||
$: name = role?.name
|
||||
|
||||
let permissions: Permission[] = []
|
||||
const permissionsQuery = createQuery()
|
||||
$: if (role !== undefined) {
|
||||
permissionsQuery.query(core.class.Permission, { _id: { $in: role.permissions } }, (res) => {
|
||||
permissions = res
|
||||
})
|
||||
}
|
||||
|
||||
function handleEditPermissions (evt: Event): void {
|
||||
if (role === undefined || descriptor === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(
|
||||
ObjectBoxPopup,
|
||||
{
|
||||
_class: core.class.Permission,
|
||||
docQuery: { _id: { $in: descriptor.availablePermissions } },
|
||||
multiSelect: true,
|
||||
allowDeselect: true,
|
||||
selectedObjects: role.permissions
|
||||
},
|
||||
evt.target as HTMLElement,
|
||||
undefined,
|
||||
async (result) => {
|
||||
if (role === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateCollection(
|
||||
core.class.Role,
|
||||
core.space.Model,
|
||||
role._id,
|
||||
spaceType._id,
|
||||
spaceType._class,
|
||||
'roles',
|
||||
{
|
||||
permissions: result
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if role !== undefined}
|
||||
<div class="hulyComponent-content__container columns">
|
||||
<div class="hulyComponent-content__column content">
|
||||
<Scroller align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}>
|
||||
<div class="hulyComponent-content gap">
|
||||
<div class="hulyComponent-content__column-group mt-4">
|
||||
<div class="hulyComponent-content__header mb-6">
|
||||
<ButtonIcon icon={PersonIcon} size="large" iconProps={{ size: 'small' }} kind="secondary" />
|
||||
<AttributeEditor _class={core.class.Role} object={role} key="name" editKind="modern-ghost-large" />
|
||||
</div>
|
||||
|
||||
<div class="hulyTableAttr-container">
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<IconSettings size="small" />
|
||||
<span><Label label={settingRes.string.Permissions} /></span>
|
||||
<ButtonIcon kind="primary" icon={IconEdit} size="small" on:click={handleEditPermissions} />
|
||||
</div>
|
||||
|
||||
{#if permissions.length > 0}
|
||||
<div class="hulyTableAttr-content task">
|
||||
{#each permissions as permission}
|
||||
<div class="hulyTableAttr-content__row">
|
||||
{#if permission.icon !== undefined}
|
||||
<div class="hulyTableAttr-content__row-icon-wrapper">
|
||||
<Icon icon={permission.icon} size="small" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="hulyTableAttr-content__row-label font-medium-14">
|
||||
<Label label={permission.label} />
|
||||
</div>
|
||||
|
||||
{#if permission.description !== undefined}
|
||||
<div class="hulyTableAttr-content__row-label grow dark font-regular-14">
|
||||
<Label label={permission.description} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
@ -1,6 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
// 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
|
||||
@ -14,47 +13,47 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { ProjectType } from '@hcengineering/task'
|
||||
import { Ref, SpaceType, WithLookup } from '@hcengineering/core'
|
||||
import { Icon, Label, IconOpenedArrow } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let typeId: Ref<ProjectType> | undefined
|
||||
|
||||
export let types: WithLookup<ProjectType>[] = []
|
||||
export let selectedTypeId: Ref<SpaceType> | undefined
|
||||
export let types: WithLookup<SpaceType>[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
function select (item: ProjectType): void {
|
||||
typeId = item._id
|
||||
dispatch('change', typeId)
|
||||
|
||||
function handleSelected (type: SpaceType): void {
|
||||
selectedTypeId = type._id
|
||||
|
||||
dispatch('change', selectedTypeId)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each types as typeItem}
|
||||
{#each types as type}
|
||||
{@const descriptor = type.$lookup?.descriptor}
|
||||
<button
|
||||
class="hulyTaskNavLink-container font-regular-14"
|
||||
class:selected={typeItem._id === typeId}
|
||||
class:selected={type._id === selectedTypeId}
|
||||
on:click={() => {
|
||||
select(typeItem)
|
||||
handleSelected(type)
|
||||
}}
|
||||
>
|
||||
<div class="hulyTaskNavLink-avatar">
|
||||
{#if typeItem.$lookup?.descriptor?.icon}
|
||||
{#if descriptor?.icon}
|
||||
<div class="hulyTaskNavLink-icon">
|
||||
<Icon icon={typeItem.$lookup?.descriptor?.icon} size={'small'} fill={'currentColor'} />
|
||||
<Icon icon={descriptor?.icon} size="small" fill="currentColor" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if typeItem.$lookup?.descriptor}
|
||||
{#if descriptor}
|
||||
<div class="hulyTaskNavLink-content">
|
||||
<span class="hulyTaskNavLink-content__title">{typeItem.name}</span>
|
||||
<span class="hulyTaskNavLink-content__title">{type.name}</span>
|
||||
<span class="hulyTaskNavLink-content__descriptor">
|
||||
<Label label={typeItem.$lookup?.descriptor.name} />
|
||||
<Label label={descriptor.name} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if typeItem._id === typeId}
|
||||
{#if type._id === selectedTypeId}
|
||||
<div class="hulyTaskNavLink-icon right">
|
||||
<IconOpenedArrow size={'small'} />
|
||||
</div>
|
@ -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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import setting from '@hcengineering/setting'
|
||||
import { Modal, ModernEditbox } from '@hcengineering/ui'
|
||||
import core, { AttachedData, Attribute, PropertyType, Ref, Role, SpaceType } from '@hcengineering/core'
|
||||
import { ArrOf, TypeRef } from '@hcengineering/model'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
|
||||
import settingRes from '../../../plugin'
|
||||
import { clearSettingsStore } from '../../../store'
|
||||
|
||||
const client = getClient()
|
||||
export let type: SpaceType
|
||||
|
||||
const roleData: AttachedData<Role> = {
|
||||
name: '',
|
||||
permissions: []
|
||||
}
|
||||
|
||||
async function handleRoleCreated (): Promise<void> {
|
||||
const name = roleData.name.trim()
|
||||
if (name === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const roleId = await client.addCollection(
|
||||
core.class.Role,
|
||||
core.space.Model,
|
||||
type._id,
|
||||
type._class,
|
||||
'roles',
|
||||
roleData
|
||||
)
|
||||
|
||||
// Create role as an attribute of space type's mixin
|
||||
await client.createDoc(
|
||||
core.class.Attribute,
|
||||
core.space.Model,
|
||||
{
|
||||
name: roleId,
|
||||
attributeOf: type.targetClass,
|
||||
type: ArrOf(TypeRef(core.class.Account)),
|
||||
label: getEmbeddedLabel(`Role: ${name}`),
|
||||
editor: setting.component.RoleAssignmentEditor
|
||||
},
|
||||
`role-${roleId}` as Ref<Attribute<PropertyType>>
|
||||
)
|
||||
}
|
||||
|
||||
$: canSave = roleData.name.trim() !== ''
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
label={settingRes.string.Role}
|
||||
type="type-aside"
|
||||
okAction={handleRoleCreated}
|
||||
{canSave}
|
||||
okLabel={presentation.string.Create}
|
||||
on:changeContent
|
||||
onCancel={() => {
|
||||
clearSettingsStore()
|
||||
}}
|
||||
>
|
||||
<div class="hulyModal-content__titleGroup">
|
||||
<ModernEditbox bind:value={roleData.name} label={settingRes.string.RoleName} size="large" kind="ghost" autoFocus />
|
||||
</div>
|
||||
</Modal>
|
@ -0,0 +1,106 @@
|
||||
<!--
|
||||
// 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 { getClient } from '@hcengineering/presentation'
|
||||
import core, { SpaceType, SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { SpaceTypeEditor } from '@hcengineering/setting'
|
||||
import {
|
||||
ButtonIcon,
|
||||
Component,
|
||||
IconDescription,
|
||||
NavItem,
|
||||
Scroller,
|
||||
Separator,
|
||||
defineSeparators,
|
||||
secondNavSeparators
|
||||
} from '@hcengineering/ui'
|
||||
|
||||
export let type: SpaceType
|
||||
export let descriptor: SpaceTypeDescriptor | undefined
|
||||
export let editorDescriptor: SpaceTypeEditor
|
||||
export let visibleSecondNav: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
|
||||
$: if (descriptor === undefined) {
|
||||
void loadDescriptor()
|
||||
}
|
||||
|
||||
async function loadDescriptor (): Promise<void> {
|
||||
descriptor = await client.findOne(core.class.SpaceTypeDescriptor, { _id: type.descriptor })
|
||||
}
|
||||
|
||||
const navigator: {
|
||||
id: string
|
||||
label: IntlString
|
||||
}[] =
|
||||
editorDescriptor !== undefined
|
||||
? editorDescriptor.sections.map(({ id, label }) => ({
|
||||
id,
|
||||
label
|
||||
}))
|
||||
: []
|
||||
|
||||
const sectionRefs: Record<string, HTMLElement | undefined> = {}
|
||||
|
||||
defineSeparators('spaceTypeEditor', secondNavSeparators)
|
||||
</script>
|
||||
|
||||
{#if type !== undefined && descriptor !== undefined}
|
||||
<div class="hulyComponent-content__container columns">
|
||||
{#if visibleSecondNav}
|
||||
<div class="hulyComponent-content__column">
|
||||
<div class="hulyComponent-content__navHeader">
|
||||
<div class="hulyComponent-content__navHeader-menu">
|
||||
<ButtonIcon kind="tertiary" icon={IconDescription} size="small" inheritColor />
|
||||
</div>
|
||||
</div>
|
||||
{#each navigator as navItem, i (navItem.id)}
|
||||
<NavItem
|
||||
type="type-anchor-link"
|
||||
label={navItem.label}
|
||||
on:click={() => {
|
||||
sectionRefs[navItem.id]?.scrollIntoView()
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<Separator name="spaceTypeEditor" index={0} color="transparent" />
|
||||
{/if}
|
||||
{#if editorDescriptor !== undefined}
|
||||
<div class="hulyComponent-content__column content">
|
||||
<Scroller align="center" padding="var(--spacing-3)" bottomPadding="var(--spacing-3)">
|
||||
<div class="hulyComponent-content gap">
|
||||
{#each editorDescriptor.sections as section}
|
||||
<div bind:this={sectionRefs[section.id]} class:hulyTableAttr-container={!section.withoutContainer}>
|
||||
<Component
|
||||
is={section.component}
|
||||
props={{
|
||||
type,
|
||||
descriptor
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
@ -0,0 +1,92 @@
|
||||
<!--
|
||||
// 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 core, { type SpaceType, type SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
import { ButtonIcon, IconSquareExpand, ModernButton, ModernEditbox, TextArea } from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
|
||||
import settingRes from '../../../plugin'
|
||||
|
||||
export let type: SpaceType | undefined
|
||||
export let descriptor: SpaceTypeDescriptor | undefined
|
||||
|
||||
const client = getClient()
|
||||
let shortDescription = type?.shortDescription ?? ''
|
||||
|
||||
let spacesCount: number = 0
|
||||
const spacesCountQuery = createQuery()
|
||||
$: if (type !== undefined) {
|
||||
spacesCountQuery.query(
|
||||
core.class.TypedSpace,
|
||||
{ type: type._id },
|
||||
(res) => {
|
||||
spacesCount = res.length
|
||||
},
|
||||
{
|
||||
projection: { _id: 1 }
|
||||
}
|
||||
)
|
||||
} else {
|
||||
spacesCountQuery.unsubscribe()
|
||||
}
|
||||
|
||||
async function attributeUpdated<T extends keyof SpaceType> (field: T, value: SpaceType[T]): Promise<void> {
|
||||
if (type === undefined || type[field] === value) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(type, { [field]: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if descriptor !== undefined}
|
||||
<div class="hulyComponent-content__column-group">
|
||||
<div class="hulyComponent-content__header">
|
||||
<div class="flex gap-1">
|
||||
<ButtonIcon icon={descriptor.icon} size={'large'} kind={'secondary'} />
|
||||
<ModernEditbox
|
||||
kind="ghost"
|
||||
size="large"
|
||||
label={settingRes.string.SpaceTypeTitle}
|
||||
value={type?.name ?? ''}
|
||||
on:blur={(evt) => {
|
||||
attributeUpdated('name', evt.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ModernButton
|
||||
icon={IconSquareExpand}
|
||||
label={settingRes.string.CountSpaces}
|
||||
labelParams={{ count: spacesCount }}
|
||||
disabled={spacesCount === 0}
|
||||
kind="tertiary"
|
||||
size="medium"
|
||||
hasMenu
|
||||
/>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={settingRes.string.Description}
|
||||
width="100%"
|
||||
height="4.5rem"
|
||||
margin="var(--spacing-2) 0"
|
||||
noFocusBorder
|
||||
bind:value={shortDescription}
|
||||
on:change={() => {
|
||||
attributeUpdated('shortDescription', shortDescription)
|
||||
}}
|
||||
/>
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
{/if}
|
@ -0,0 +1,26 @@
|
||||
<!--
|
||||
// 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 { type SpaceType, type SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
|
||||
import ClassAttributes from '../../ClassAttributes.svelte'
|
||||
|
||||
export let type: SpaceType | undefined
|
||||
export let descriptor: SpaceTypeDescriptor | undefined
|
||||
</script>
|
||||
|
||||
{#if type !== undefined && descriptor !== undefined}
|
||||
<ClassAttributes ofClass={descriptor.baseClass} _class={type.targetClass} showHierarchy />
|
||||
{/if}
|
@ -0,0 +1,91 @@
|
||||
<!--
|
||||
// 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 core, { Role, SortingOrder, SpaceType, SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
import { ButtonIcon, IconAdd, Label, getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
|
||||
import MembersIcon from '../../icons/Members.svelte'
|
||||
import PersonIcon from '../../icons/Person.svelte'
|
||||
import CreateRole from './CreateRole.svelte'
|
||||
import { clearSettingsStore, settingsStore } from '../../../store'
|
||||
import settingRes from '../../../plugin'
|
||||
|
||||
export let type: SpaceType | undefined
|
||||
export let descriptor: SpaceTypeDescriptor | undefined
|
||||
|
||||
let roles: Role[] = []
|
||||
const rolesQuery = createQuery()
|
||||
$: if (type !== undefined) {
|
||||
rolesQuery.query(
|
||||
core.class.Role,
|
||||
{ attachedTo: type._id },
|
||||
(res) => {
|
||||
roles = res
|
||||
},
|
||||
{ sort: { _id: SortingOrder.Ascending } }
|
||||
)
|
||||
}
|
||||
|
||||
function handleRoleSelected (id: string | undefined): void {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
if (id !== undefined) {
|
||||
loc.path[5] = 'roles'
|
||||
loc.path[6] = id
|
||||
loc.path.length = 7
|
||||
} else {
|
||||
loc.path.length = 5
|
||||
}
|
||||
|
||||
clearSettingsStore()
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if descriptor !== undefined}
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<MembersIcon size="small" />
|
||||
<span><Label label={settingRes.string.Roles} /></span>
|
||||
<ButtonIcon
|
||||
kind="primary"
|
||||
icon={IconAdd}
|
||||
size="small"
|
||||
on:click={(ev) => {
|
||||
$settingsStore = { id: 'createRole', component: CreateRole, props: { type, descriptor } }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if roles.length}
|
||||
<div class="hulyTableAttr-content task">
|
||||
{#each roles as role}
|
||||
<button
|
||||
class="hulyTableAttr-content__row justify-start"
|
||||
on:click|stopPropagation={() => {
|
||||
handleRoleSelected(role._id)
|
||||
}}
|
||||
>
|
||||
<div class="hulyTableAttr-content__row-icon-wrapper">
|
||||
<PersonIcon size="small" />
|
||||
</div>
|
||||
{#if role.name !== ''}
|
||||
<div class="hulyTableAttr-content__row-label font-medium-14">
|
||||
{role.name}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
@ -0,0 +1,42 @@
|
||||
<!--
|
||||
// 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 { AccountArrayEditor } from '@hcengineering/contact-resources'
|
||||
import { Account, Ref, TypedSpace } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
|
||||
export let object: TypedSpace
|
||||
export let label: IntlString
|
||||
export let value: Ref<Account>[]
|
||||
export let onChange: ((refs: Ref<Account>[]) => void) | undefined
|
||||
export let readonly = false
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let width: string | undefined = undefined
|
||||
|
||||
$: members = object?.members ?? []
|
||||
</script>
|
||||
|
||||
<AccountArrayEditor
|
||||
{value}
|
||||
{label}
|
||||
readonly={readonly || (object.private && members.length === 0)}
|
||||
includeItems={object.private ? members : undefined}
|
||||
{onChange}
|
||||
{size}
|
||||
width={width ?? 'min-content'}
|
||||
{kind}
|
||||
/>
|
@ -49,8 +49,18 @@ import IntegrationPanel from './components/IntegrationPanel.svelte'
|
||||
import { getOwnerFirstName, getOwnerLastName, getOwnerPosition, getValue, filterDescendants } from './utils'
|
||||
import ClassAttributes from './components/ClassAttributes.svelte'
|
||||
import ClassAttributesList from './components/ClassAttributesList.svelte'
|
||||
import ManageSpaceTypes from './components/spaceTypes/ManageSpaceTypes.svelte'
|
||||
import ManageSpaceTypesTools from './components/spaceTypes/ManageSpaceTypesTools.svelte'
|
||||
import ManageSpaceTypeContent from './components/spaceTypes/ManageSpaceTypeContent.svelte'
|
||||
import PermissionPresenter from './components/presenters/PermissionPresenter.svelte'
|
||||
import SpaceTypeDescriptorPresenter from './components/presenters/SpaceTypeDescriptorPresenter.svelte'
|
||||
import SpaceTypeGeneralSectionEditor from './components/spaceTypes/editor/SpaceTypeGeneralSectionEditor.svelte'
|
||||
import SpaceTypePropertiesSectionEditor from './components/spaceTypes/editor/SpaceTypePropertiesSectionEditor.svelte'
|
||||
import SpaceTypeRolesSectionEditor from './components/spaceTypes/editor/SpaceTypeRolesSectionEditor.svelte'
|
||||
import RoleEditor from './components/spaceTypes/RoleEditor.svelte'
|
||||
import RoleAssignmentEditor from './components/typeEditors/RoleAssignmentEditor.svelte'
|
||||
|
||||
export { ClassSetting, filterDescendants, ClassAttributes, ClassAttributesList }
|
||||
export { ClassSetting, filterDescendants, ClassAttributes, ClassAttributesList, SpaceTypeGeneralSectionEditor }
|
||||
export * from './store'
|
||||
|
||||
async function DeleteMixin (object: Mixin<Class<Doc>>): Promise<void> {
|
||||
@ -106,7 +116,17 @@ export default async (): Promise<Resources> => ({
|
||||
CreateMixin,
|
||||
InviteSetting,
|
||||
IntegrationPanel,
|
||||
Configure
|
||||
Configure,
|
||||
ManageSpaceTypes,
|
||||
ManageSpaceTypesTools,
|
||||
ManageSpaceTypeContent,
|
||||
PermissionPresenter,
|
||||
SpaceTypeDescriptorPresenter,
|
||||
SpaceTypeGeneralSectionEditor,
|
||||
SpaceTypePropertiesSectionEditor,
|
||||
SpaceTypeRolesSectionEditor,
|
||||
RoleEditor,
|
||||
RoleAssignmentEditor
|
||||
},
|
||||
actionImpl: {
|
||||
DeleteMixin
|
||||
|
@ -20,7 +20,10 @@ import { type AnyComponent } from '@hcengineering/ui'
|
||||
|
||||
export default mergeIds(settingId, setting, {
|
||||
component: {
|
||||
EditEnum: '' as AnyComponent
|
||||
EditEnum: '' as AnyComponent,
|
||||
ManageSpaceTypes: '' as AnyComponent,
|
||||
ManageSpaceTypesTools: '' as AnyComponent,
|
||||
ManageSpaceTypeContent: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
IntegrationDisabled: '' as IntlString,
|
||||
@ -88,6 +91,12 @@ export default mergeIds(settingId, setting, {
|
||||
ConfigBeta: '' as IntlString,
|
||||
ClassSettingHint: '' as IntlString,
|
||||
ClassProperties: '' as IntlString,
|
||||
ClassColon: '' as IntlString
|
||||
ClassColon: '' as IntlString,
|
||||
NewSpaceType: '' as IntlString,
|
||||
SpaceTypeTitle: '' as IntlString,
|
||||
Description: '' as IntlString,
|
||||
CountSpaces: '' as IntlString,
|
||||
RoleName: '' as IntlString,
|
||||
Permissions: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -19,6 +19,11 @@ import { Asset, IntlString, plugin, Resource } from '@hcengineering/platform'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { TemplateFieldCategory, TemplateField } from '@hcengineering/templates'
|
||||
|
||||
import { SpaceTypeEditor, SpaceTypeCreator } from './spaceTypeEditor'
|
||||
|
||||
export * from './spaceTypeEditor'
|
||||
export * from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -122,11 +127,14 @@ export default plugin(settingId, {
|
||||
ClassSetting: '' as Ref<Doc>,
|
||||
Owners: '' as Ref<Doc>,
|
||||
InviteSettings: '' as Ref<Doc>,
|
||||
WorkspaceSetting: '' as Ref<Doc>
|
||||
WorkspaceSetting: '' as Ref<Doc>,
|
||||
ManageSpaces: '' as Ref<Doc>
|
||||
},
|
||||
mixin: {
|
||||
Editable: '' as Ref<Mixin<Editable>>,
|
||||
UserMixin: '' as Ref<Mixin<UserMixin>>
|
||||
UserMixin: '' as Ref<Mixin<UserMixin>>,
|
||||
SpaceTypeEditor: '' as Ref<Mixin<SpaceTypeEditor>>,
|
||||
SpaceTypeCreator: '' as Ref<Mixin<SpaceTypeCreator>>
|
||||
},
|
||||
space: {
|
||||
Setting: '' as Ref<Space>
|
||||
@ -149,7 +157,14 @@ export default plugin(settingId, {
|
||||
Support: '' as AnyComponent,
|
||||
Privacy: '' as AnyComponent,
|
||||
Terms: '' as AnyComponent,
|
||||
ClassSetting: '' as AnyComponent
|
||||
ClassSetting: '' as AnyComponent,
|
||||
PermissionPresenter: '' as AnyComponent,
|
||||
SpaceTypeDescriptorPresenter: '' as AnyComponent,
|
||||
SpaceTypeGeneralSectionEditor: '' as AnyComponent,
|
||||
SpaceTypePropertiesSectionEditor: '' as AnyComponent,
|
||||
SpaceTypeRolesSectionEditor: '' as AnyComponent,
|
||||
RoleEditor: '' as AnyComponent,
|
||||
RoleAssignmentEditor: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Settings: '' as IntlString,
|
||||
@ -182,10 +197,13 @@ export default plugin(settingId, {
|
||||
Owners: '' as IntlString,
|
||||
Configure: '' as IntlString,
|
||||
InviteSettings: '' as IntlString,
|
||||
General: '' as IntlString,
|
||||
Properties: '' as IntlString,
|
||||
TaskTypes: '' as IntlString,
|
||||
Automations: '' as IntlString,
|
||||
Collections: '' as IntlString
|
||||
Collections: '' as IntlString,
|
||||
SpaceTypes: '' as IntlString,
|
||||
Roles: '' as IntlString
|
||||
},
|
||||
icon: {
|
||||
AccountSettings: '' as Asset,
|
||||
|
49
plugins/setting/src/spaceTypeEditor.ts
Normal file
49
plugins/setting/src/spaceTypeEditor.ts
Normal file
@ -0,0 +1,49 @@
|
||||
//
|
||||
// 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 type { Class, SpaceType, SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* A mixin describing various configurations of a space type editor
|
||||
*/
|
||||
export interface SpaceTypeEditor extends Class<SpaceType> {
|
||||
sections: SpaceTypeEditorSection[]
|
||||
subEditors?: Record<string, AnyComponent>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Describes one space type editor section
|
||||
*/
|
||||
export interface SpaceTypeEditorSection {
|
||||
id: string
|
||||
label: IntlString
|
||||
component: AnyComponent
|
||||
withoutContainer?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* A mixin for extensions during space type creation
|
||||
*/
|
||||
export interface SpaceTypeCreator extends Class<SpaceTypeDescriptor> {
|
||||
extraComponent: AnyComponent
|
||||
}
|
55
plugins/setting/src/utils.ts
Normal file
55
plugins/setting/src/utils.ts
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// 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 core, { Class, ClassifierKind, Data, Doc, Ref, SpaceType, TxOperations, generateId } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
|
||||
export async function createSpaceType (
|
||||
client: TxOperations,
|
||||
data: Omit<Data<SpaceType>, 'targetClass'>,
|
||||
_id: Ref<SpaceType>
|
||||
): Promise<Ref<SpaceType>> {
|
||||
const descriptorObj = client.getModel().findObject(data.descriptor)
|
||||
if (descriptorObj === undefined) {
|
||||
throw new Error('Descriptor is not found in the model')
|
||||
}
|
||||
|
||||
const baseClassClazz = client.getHierarchy().getClass(descriptorObj.baseClass)
|
||||
const spaceTypeMixinId: Ref<Class<Doc>> = generateId()
|
||||
await client.createDoc(
|
||||
core.class.Mixin,
|
||||
core.space.Model,
|
||||
{
|
||||
extends: descriptorObj.baseClass,
|
||||
kind: ClassifierKind.MIXIN,
|
||||
label: getEmbeddedLabel(data.name),
|
||||
icon: baseClassClazz.icon
|
||||
},
|
||||
spaceTypeMixinId
|
||||
)
|
||||
|
||||
return await client.createDoc(
|
||||
core.class.SpaceType,
|
||||
core.space.Model,
|
||||
{
|
||||
shortDescription: data.shortDescription,
|
||||
descriptor: data.descriptor,
|
||||
roles: data.roles,
|
||||
name: data.name,
|
||||
targetClass: spaceTypeMixinId
|
||||
},
|
||||
_id
|
||||
)
|
||||
}
|
@ -97,6 +97,8 @@
|
||||
"Color": "Color",
|
||||
"Identifier": "Identifier",
|
||||
"RenameStatus": "Rename a status to new name",
|
||||
"UpdateTasksStatusRequest": "Status is used with {total} tasks, it will require update of all of them. Please approve."
|
||||
"UpdateTasksStatusRequest": "Status is used with {total} tasks, it will require update of all of them. Please approve.",
|
||||
"TaskTypes": "Task types",
|
||||
"Collections": "Collections"
|
||||
}
|
||||
}
|
@ -97,6 +97,8 @@
|
||||
"Color": "Цвет",
|
||||
"Identifier": "Идентификатор",
|
||||
"RenameStatus": "Переименование статуса в новое имя",
|
||||
"UpdateTasksStatusRequest": "Статус сейчас используется в {total} задачах, потребуется обновление всеи их. Пожалуйста подтвердите."
|
||||
"UpdateTasksStatusRequest": "Статус сейчас используется в {total} задачах, потребуется обновление всеи их. Пожалуйста подтвердите.",
|
||||
"TaskTypes": "Типы задач",
|
||||
"Collections": "Коллекции"
|
||||
}
|
||||
}
|
@ -14,72 +14,45 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { Ref, generateId } from '@hcengineering/core'
|
||||
import { Card, getClient, hasResource } from '@hcengineering/presentation'
|
||||
import { SpaceTypeDescriptor, generateId } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { ProjectTypeDescriptor, createProjectType } from '@hcengineering/task'
|
||||
import { DropdownLabelsIntl, EditBox, ToggleWithLabel } from '@hcengineering/ui'
|
||||
import { ToggleWithLabel } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../../plugin'
|
||||
import { Resource } from '@hcengineering/platform'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let name: string = ''
|
||||
let classic: boolean = true
|
||||
export let descriptor: SpaceTypeDescriptor
|
||||
export let name: string = ''
|
||||
export const handleTypeCreated: () => Promise<void> = createType
|
||||
|
||||
let descriptor: ProjectTypeDescriptor | undefined = undefined
|
||||
let classic: boolean = true
|
||||
$: projDescriptor = descriptor as ProjectTypeDescriptor
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function createType (): Promise<void> {
|
||||
if (descriptor === undefined) {
|
||||
if (projDescriptor === undefined) {
|
||||
return
|
||||
}
|
||||
await createProjectType(
|
||||
client,
|
||||
{
|
||||
name,
|
||||
descriptor: descriptor._id,
|
||||
descriptor: projDescriptor._id,
|
||||
description: '',
|
||||
tasks: [],
|
||||
classic: descriptor.allowedClassic === true ? classic : false
|
||||
roles: 0,
|
||||
classic: projDescriptor.allowedClassic === true ? classic : false
|
||||
},
|
||||
[],
|
||||
generateId()
|
||||
)
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
const descriptors = client
|
||||
.getModel()
|
||||
.findAllSync(task.class.ProjectTypeDescriptor, {})
|
||||
.filter((p) => hasResource(p._id as any as Resource<any>))
|
||||
const items = descriptors.map((it) => ({
|
||||
label: it.name,
|
||||
id: it._id
|
||||
}))
|
||||
|
||||
function selectType (evt: CustomEvent<Ref<ProjectTypeDescriptor>>): void {
|
||||
descriptor = descriptors.find((it) => it._id === evt.detail)
|
||||
}
|
||||
|
||||
descriptor = descriptors[0]
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={task.string.CreateProjectType}
|
||||
canSave={name.trim().length > 0 && descriptor !== undefined}
|
||||
okAction={createType}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
on:changeContent
|
||||
>
|
||||
<div class="flex-col flex-gap-2">
|
||||
<EditBox bind:value={name} placeholder={task.string.ProjectType} />
|
||||
<DropdownLabelsIntl {items} on:selected={selectType} />
|
||||
{#if descriptor?.allowedClassic === true}
|
||||
<ToggleWithLabel label={task.string.ClassicProject} bind:on={classic} />
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
{#if projDescriptor?.allowedClassic === true}
|
||||
<ToggleWithLabel label={task.string.ClassicProject} bind:on={classic} />
|
||||
{/if}
|
||||
|
@ -1,51 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022, 2023 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 { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { ProjectType } from '@hcengineering/task'
|
||||
import { Location, resolvedLocationStore, resizeObserver } from '@hcengineering/ui'
|
||||
|
||||
import { onDestroy } from 'svelte'
|
||||
import { typeStore } from '../../'
|
||||
import ProjectEditor from './ProjectEditor.svelte'
|
||||
|
||||
export let visibleNav: boolean = true
|
||||
|
||||
let visibleSecondNav: boolean = true
|
||||
let type: WithLookup<ProjectType> | undefined
|
||||
let typeId: Ref<ProjectType> | undefined
|
||||
|
||||
onDestroy(
|
||||
resolvedLocationStore.subscribe((loc) => {
|
||||
void (async (loc: Location): Promise<void> => {
|
||||
typeId = loc.path[4] as Ref<ProjectType>
|
||||
})(loc)
|
||||
})
|
||||
)
|
||||
|
||||
$: type = typeId !== undefined ? $typeStore.get(typeId) : undefined
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="hulyComponent"
|
||||
use:resizeObserver={(element) => {
|
||||
visibleSecondNav = element.clientWidth > 720
|
||||
}}
|
||||
>
|
||||
{#if type !== undefined}
|
||||
<ProjectEditor {type} descriptor={type.$lookup?.descriptor} {visibleNav} {visibleSecondNav} on:change />
|
||||
{/if}
|
||||
</div>
|
@ -1,353 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022, 2023 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 { ComponentExtensions, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import task, { Project, ProjectType, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
|
||||
import { Ref, SortingOrder } from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
import { ClassAttributes, clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
ButtonIcon,
|
||||
Component,
|
||||
Header,
|
||||
IconAdd,
|
||||
IconCopy,
|
||||
IconDelete,
|
||||
IconDescription,
|
||||
IconFolder,
|
||||
IconMoreV,
|
||||
IconSquareExpand,
|
||||
Label,
|
||||
Location,
|
||||
ModernButton,
|
||||
ModernEditbox,
|
||||
NavItem,
|
||||
Scroller,
|
||||
Separator,
|
||||
TextArea,
|
||||
defineSeparators,
|
||||
getCurrentResolvedLocation,
|
||||
navigate,
|
||||
resolvedLocationStore,
|
||||
secondNavSeparators
|
||||
} from '@hcengineering/ui'
|
||||
import { showMenu } from '@hcengineering/view-resources'
|
||||
import plugin from '../../plugin'
|
||||
import IconLayers from '../icons/Layers.svelte'
|
||||
import CreateTaskType from '../taskTypes/CreateTaskType.svelte'
|
||||
import TaskTypeEditor from '../taskTypes/TaskTypeEditor.svelte'
|
||||
import TaskTypeIcon from '../taskTypes/TaskTypeIcon.svelte'
|
||||
import TaskTypeKindEditor from '../taskTypes/TaskTypeKindEditor.svelte'
|
||||
|
||||
export let type: ProjectType
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
export let visibleNav: boolean = true
|
||||
export let visibleSecondNav: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
|
||||
const projectQuery = createQuery()
|
||||
|
||||
$: if (descriptor === undefined) {
|
||||
void client.findOne(task.class.ProjectTypeDescriptor, { _id: type.descriptor }).then((res) => {
|
||||
descriptor = res
|
||||
})
|
||||
}
|
||||
|
||||
let taskTypes: TaskType[] = []
|
||||
let projects: Project[] = []
|
||||
|
||||
$: query.query(
|
||||
task.class.TaskType,
|
||||
{ _id: { $in: type?.tasks ?? [] } },
|
||||
(res) => {
|
||||
taskTypes = res
|
||||
},
|
||||
{ sort: { _id: SortingOrder.Ascending } }
|
||||
)
|
||||
async function onShortDescriptionChange (value: string): Promise<void> {
|
||||
if (type !== undefined) {
|
||||
await client.diffUpdate(type, { shortDescription: value })
|
||||
}
|
||||
}
|
||||
|
||||
$: projectQuery.query(task.class.Project, { type: type._id }, (res) => {
|
||||
projects = res
|
||||
})
|
||||
|
||||
const statsQuery = createQuery()
|
||||
|
||||
let tasks: Task[] = []
|
||||
|
||||
$: statsQuery.query(
|
||||
task.class.Task,
|
||||
{ kind: { $in: taskTypes.map((it) => it._id) } },
|
||||
(res) => {
|
||||
tasks = res
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
_class: 1,
|
||||
space: 1,
|
||||
status: 1,
|
||||
kind: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// $: spaceCounter = tasks.reduce(
|
||||
// (map, task) => map.set(task.space, (map.get(task.space) ?? 0) + 1),
|
||||
// new Map<Ref<Space>, number>()
|
||||
// )
|
||||
|
||||
$: taskTypeCounter = tasks.reduce(
|
||||
(map, task) => map.set(task.kind, (map.get(task.kind) ?? 0) + 1),
|
||||
new Map<Ref<TaskType>, number>()
|
||||
)
|
||||
|
||||
let selectedTaskTypeId: Ref<TaskType> | undefined
|
||||
|
||||
$: selectedTaskType = taskTypes.find((it) => it._id === selectedTaskTypeId)
|
||||
|
||||
onDestroy(
|
||||
resolvedLocationStore.subscribe((loc) => {
|
||||
void (async (loc: Location): Promise<void> => {
|
||||
selectedTaskTypeId = loc.path[5] as Ref<TaskType>
|
||||
})(loc)
|
||||
})
|
||||
)
|
||||
|
||||
function selectTaskType (id: string | undefined): void {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
if (id !== undefined) {
|
||||
loc.path[5] = id
|
||||
loc.path.length = 6
|
||||
} else {
|
||||
loc.path.length = 5
|
||||
}
|
||||
selectedTaskTypeId = id as Ref<TaskType>
|
||||
clearSettingsStore()
|
||||
navigate(loc)
|
||||
}
|
||||
$: items =
|
||||
selectedTaskType !== undefined
|
||||
? [
|
||||
{ label: plugin.string.ProjectType, icon: descriptor?.icon },
|
||||
{ title: selectedTaskType.name, icon: selectedTaskType.icon }
|
||||
]
|
||||
: [{ label: plugin.string.ProjectType, icon: descriptor?.icon }]
|
||||
|
||||
let scroller: Scroller
|
||||
const navigator: {
|
||||
id: string
|
||||
label: IntlString
|
||||
element?: HTMLElement
|
||||
}[] = [
|
||||
{ id: 'properties', label: setting.string.Properties },
|
||||
{ id: 'tasktypes', label: setting.string.TaskTypes },
|
||||
{ id: 'automations', label: setting.string.Automations },
|
||||
{ id: 'collections', label: setting.string.Collections }
|
||||
]
|
||||
|
||||
defineSeparators('typeSettings', secondNavSeparators)
|
||||
</script>
|
||||
|
||||
{#if type !== undefined && descriptor !== undefined}
|
||||
<Header minimize={!visibleNav} on:resize={(event) => dispatch('change', event.detail)}>
|
||||
<ButtonIcon
|
||||
icon={IconCopy}
|
||||
size={'small'}
|
||||
kind={'secondary'}
|
||||
disabled
|
||||
on:click={(ev) => {
|
||||
// Do copy of type
|
||||
}}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
size={'small'}
|
||||
kind={'secondary'}
|
||||
disabled
|
||||
on:click={(ev) => {
|
||||
// Ask for delete
|
||||
}}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={IconMoreV}
|
||||
size={'small'}
|
||||
kind={'secondary'}
|
||||
on:click={(ev) => {
|
||||
showMenu(ev, { object: type })
|
||||
}}
|
||||
/>
|
||||
<Breadcrumbs
|
||||
{items}
|
||||
size={'large'}
|
||||
selected={selectedTaskType !== undefined ? 1 : 0}
|
||||
on:select={(event) => {
|
||||
if (event.detail === 0) selectTaskType(undefined)
|
||||
}}
|
||||
>
|
||||
<!-- afterLabel={plugin.string.Published} -->
|
||||
<!-- <span slot="afterLabel">{dateStr}</span> -->
|
||||
</Breadcrumbs>
|
||||
<!-- <svelte:fragment slot="actions">
|
||||
<div class="hulyHeader-buttonsGroup__label font-regular-12">
|
||||
<Label label={plugin.string.LastSave} />
|
||||
<span>{dateStr}</span>
|
||||
</div>
|
||||
<ModernButton kind={'secondary'} label={ui.string.SaveDraft} size={'small'} />
|
||||
<ModernButton kind={'primary'} icon={IconSend} label={ui.string.Publish} size={'small'} />
|
||||
</svelte:fragment> -->
|
||||
</Header>
|
||||
<div class="hulyComponent-content__container columns">
|
||||
{#if selectedTaskTypeId === undefined && visibleSecondNav}
|
||||
<div class="hulyComponent-content__column">
|
||||
<div class="hulyComponent-content__navHeader">
|
||||
<div class="hulyComponent-content__navHeader-menu">
|
||||
<ButtonIcon kind={'tertiary'} icon={IconDescription} size={'small'} inheritColor />
|
||||
</div>
|
||||
</div>
|
||||
{#each navigator as navItem, i (navItem.id)}
|
||||
<NavItem
|
||||
type={'type-anchor-link'}
|
||||
label={navItem.label}
|
||||
on:click={() => {
|
||||
if (i === 0) scroller.scroll(0)
|
||||
else navItem.element?.scrollIntoView()
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<Separator name={'typeSettings'} index={0} color={'transparent'} />
|
||||
{/if}
|
||||
<div class="hulyComponent-content__column content">
|
||||
<Scroller bind:this={scroller} align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}>
|
||||
<div class="hulyComponent-content gap">
|
||||
{#if selectedTaskType === undefined}
|
||||
<div class="hulyComponent-content__column-group">
|
||||
<div class="hulyComponent-content__header">
|
||||
<ButtonIcon icon={descriptor.icon} size={'large'} kind={'secondary'} />
|
||||
<ModernButton
|
||||
icon={IconSquareExpand}
|
||||
label={plugin.string.CountProjects}
|
||||
labelParams={{ count: projects.length }}
|
||||
disabled={projects.length === 0}
|
||||
kind={'tertiary'}
|
||||
size={'medium'}
|
||||
hasMenu
|
||||
/>
|
||||
</div>
|
||||
<ModernEditbox
|
||||
kind={'ghost'}
|
||||
size={'large'}
|
||||
label={plugin.string.ProjectTypeTitle}
|
||||
value={type?.name ?? ''}
|
||||
on:blur={(evt) => {
|
||||
if (type !== undefined) {
|
||||
void client.diffUpdate(type, { name: evt.detail })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextArea
|
||||
placeholder={plugin.string.Description}
|
||||
width={'100%'}
|
||||
height={'4.5rem'}
|
||||
margin={'var(--spacing-1) var(--spacing-2)'}
|
||||
noFocusBorder
|
||||
bind:value={type.shortDescription}
|
||||
on:change={() => onShortDescriptionChange(type?.shortDescription ?? '')}
|
||||
/>
|
||||
{#if descriptor?.editor}
|
||||
<Component is={descriptor.editor} props={{ type }} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ClassAttributes ofClass={descriptor.baseClass} _class={type.targetClass} showHierarchy />
|
||||
|
||||
<div bind:this={navigator[1].element} class="hulyTableAttr-container">
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<IconLayers size={'small'} />
|
||||
<span><Label label={setting.string.TaskTypes} /></span>
|
||||
<ButtonIcon
|
||||
kind={'primary'}
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
on:click={(ev) => {
|
||||
$settingsStore = { id: 'createTaskType', component: CreateTaskType, props: { type, descriptor } }
|
||||
// showPopup(CreateTaskType, { type, descriptor }, 'top')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if taskTypes.length}
|
||||
<div class="hulyTableAttr-content task">
|
||||
{#each taskTypes as taskType}
|
||||
<button
|
||||
class="hulyTableAttr-content__row"
|
||||
on:click|stopPropagation={() => {
|
||||
selectTaskType(taskType._id)
|
||||
}}
|
||||
>
|
||||
<div class="hulyTableAttr-content__row-icon-wrapper">
|
||||
<TaskTypeIcon value={taskType} size={'small'} />
|
||||
</div>
|
||||
{#if taskType.name}
|
||||
<div class="hulyTableAttr-content__row-label font-medium-14">
|
||||
{taskType.name}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="hulyTableAttr-content__row-label grow dark font-regular-14">
|
||||
<TaskTypeKindEditor readonly kind={taskType.kind} />
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div bind:this={navigator[2].element} class="flex-col gapV-8">
|
||||
<ComponentExtensions extension={task.extensions.ProjectEditorExtension} props={{ type }} />
|
||||
</div>
|
||||
|
||||
<div bind:this={navigator[3].element} class="hulyTableAttr-container">
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<IconFolder size={'small'} />
|
||||
<span><Label label={setting.string.Collections} /></span>
|
||||
<ButtonIcon kind={'primary'} icon={IconAdd} size={'small'} on:click={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<TaskTypeEditor taskType={selectedTaskType} projectType={type} {taskTypes} {taskTypeCounter} />
|
||||
{/if}
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.gap {
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,26 @@
|
||||
<!--
|
||||
// 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 { ComponentExtensions } from '@hcengineering/presentation'
|
||||
import { ProjectType, ProjectTypeDescriptor } from '@hcengineering/task'
|
||||
import task from '../../plugin'
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
</script>
|
||||
|
||||
{#if descriptor !== undefined && type !== undefined}
|
||||
<ComponentExtensions extension={task.extensions.ProjectEditorExtension} props={{ type }} />
|
||||
{/if}
|
@ -0,0 +1,30 @@
|
||||
<!--
|
||||
// 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 { ButtonIcon, IconAdd, IconFolder, Label } from '@hcengineering/ui'
|
||||
import { ProjectType, ProjectTypeDescriptor } from '@hcengineering/task'
|
||||
import task from '../../plugin'
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
</script>
|
||||
|
||||
{#if descriptor !== undefined}
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<IconFolder size="small" />
|
||||
<span><Label label={task.string.Collections} /></span>
|
||||
<ButtonIcon kind="primary" icon={IconAdd} size="small" on:click={() => {}} />
|
||||
</div>
|
||||
{/if}
|
@ -0,0 +1,30 @@
|
||||
<!--
|
||||
// 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 { type ProjectType, type ProjectTypeDescriptor } from '@hcengineering/task'
|
||||
import { SpaceTypeGeneralSectionEditor } from '@hcengineering/setting-resources'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
</script>
|
||||
|
||||
<SpaceTypeGeneralSectionEditor {type} {descriptor}>
|
||||
<svelte:fragment slot="extra">
|
||||
{#if descriptor?.editor}
|
||||
<Component is={descriptor.editor} props={{ type }} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SpaceTypeGeneralSectionEditor>
|
@ -31,14 +31,9 @@
|
||||
|
||||
let types: ProjectType[] = []
|
||||
const typesQ = createQuery()
|
||||
const query = disabled
|
||||
? {
|
||||
descriptor: { $in: descriptors }
|
||||
}
|
||||
: {
|
||||
descriptor: { $in: descriptors },
|
||||
archived: false
|
||||
}
|
||||
const query = {
|
||||
descriptor: { $in: descriptors }
|
||||
}
|
||||
$: typesQ.query(task.class.ProjectType, query, (result) => {
|
||||
types = result
|
||||
})
|
||||
|
@ -0,0 +1,94 @@
|
||||
<!--
|
||||
// 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 { SortingOrder } from '@hcengineering/core'
|
||||
import { ButtonIcon, IconAdd, Label, getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { ProjectType, ProjectTypeDescriptor, TaskType } from '@hcengineering/task'
|
||||
import { clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
|
||||
|
||||
import IconLayers from '../icons/Layers.svelte'
|
||||
import TaskTypeIcon from '../taskTypes/TaskTypeIcon.svelte'
|
||||
import TaskTypeKindEditor from '../taskTypes/TaskTypeKindEditor.svelte'
|
||||
import CreateTaskType from '../taskTypes/CreateTaskType.svelte'
|
||||
import task from '../../plugin'
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
|
||||
let taskTypes: TaskType[] = []
|
||||
const taskTypesQuery = createQuery()
|
||||
$: taskTypesQuery.query(
|
||||
task.class.TaskType,
|
||||
{ _id: { $in: type?.tasks ?? [] } },
|
||||
(res) => {
|
||||
taskTypes = res
|
||||
},
|
||||
{ sort: { _id: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
function handleTaskTypeSelected (id: string | undefined): void {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
if (id !== undefined) {
|
||||
loc.path[5] = 'taskTypes'
|
||||
loc.path[6] = id
|
||||
loc.path.length = 7
|
||||
} else {
|
||||
loc.path.length = 5
|
||||
}
|
||||
|
||||
clearSettingsStore()
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if descriptor !== undefined}
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<IconLayers size={'small'} />
|
||||
<span><Label label={task.string.TaskTypes} /></span>
|
||||
<ButtonIcon
|
||||
kind="primary"
|
||||
icon={IconAdd}
|
||||
size="small"
|
||||
on:click={(ev) => {
|
||||
$settingsStore = { id: 'createTaskType', component: CreateTaskType, props: { type, descriptor } }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if taskTypes.length}
|
||||
<div class="hulyTableAttr-content task">
|
||||
{#each taskTypes as taskType}
|
||||
<button
|
||||
class="hulyTableAttr-content__row"
|
||||
on:click|stopPropagation={() => {
|
||||
handleTaskTypeSelected(taskType._id)
|
||||
}}
|
||||
>
|
||||
<div class="hulyTableAttr-content__row-icon-wrapper">
|
||||
<TaskTypeIcon value={taskType} size={'small'} />
|
||||
</div>
|
||||
{#if taskType.name}
|
||||
<div class="hulyTableAttr-content__row-label font-medium-14">
|
||||
{taskType.name}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="hulyTableAttr-content__row-label grow dark font-regular-14">
|
||||
<TaskTypeKindEditor readonly kind={taskType.kind} />
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
@ -143,7 +143,7 @@
|
||||
icon: ofClassClass.icon
|
||||
})
|
||||
|
||||
await client.createDoc(task.class.TaskType, type._id, _taskType, taskTypeId)
|
||||
await client.createDoc(task.class.TaskType, core.space.Model, _taskType, taskTypeId)
|
||||
}
|
||||
|
||||
if (!type.tasks.includes(taskTypeId)) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022, 2023 Hardcore Engineering Inc.
|
||||
// Copyright © 2021, 2022, 2023, 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
|
||||
@ -14,11 +14,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttributeEditor, getClient } from '@hcengineering/presentation'
|
||||
import { AttributeEditor, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import task, { ProjectType, TaskType, calculateStatuses, findStatusAttr } from '@hcengineering/task'
|
||||
import { Ref, Status } from '@hcengineering/core'
|
||||
import { Ref, SortingOrder, Status } from '@hcengineering/core'
|
||||
import { Asset, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { Label, showPopup, ButtonIcon, ModernButton, IconSquareExpand, IconAdd, Icon } from '@hcengineering/ui'
|
||||
import {
|
||||
Label,
|
||||
showPopup,
|
||||
ButtonIcon,
|
||||
ModernButton,
|
||||
IconSquareExpand,
|
||||
IconAdd,
|
||||
Icon,
|
||||
Scroller
|
||||
} from '@hcengineering/ui'
|
||||
import { IconPicker, statusStore } from '@hcengineering/view-resources'
|
||||
import { ClassAttributes, settingsStore } from '@hcengineering/setting-resources'
|
||||
import { taskTypeStore } from '../..'
|
||||
@ -28,31 +37,67 @@
|
||||
import TaskTypeIcon from './TaskTypeIcon.svelte'
|
||||
import plugin from '../../plugin'
|
||||
|
||||
export let projectType: ProjectType
|
||||
export let taskType: TaskType
|
||||
export let taskTypeCounter: Map<Ref<TaskType>, number>
|
||||
export let taskTypes: TaskType[]
|
||||
export let spaceType: ProjectType
|
||||
export let objectId: Ref<TaskType>
|
||||
export let name: string | undefined
|
||||
export let icon: Asset | undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
$: descriptor = client.getModel().findAllSync(task.class.TaskTypeDescriptor, { _id: taskType?.descriptor })
|
||||
let taskTypes: TaskType[] = []
|
||||
const taskTypesQuery = createQuery()
|
||||
$: taskTypesQuery.query(
|
||||
task.class.TaskType,
|
||||
{ _id: { $in: spaceType?.tasks ?? [] } },
|
||||
(res) => {
|
||||
taskTypes = res
|
||||
},
|
||||
{ sort: { _id: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
$: states = taskType.statuses.map((p) => $statusStore.byId.get(p)).filter((p) => p !== undefined) as Status[]
|
||||
$: taskType = taskTypes.find((tt) => tt._id === objectId)
|
||||
$: name = taskType?.name
|
||||
$: icon = taskType?.icon
|
||||
$: descriptor = client.getModel().findAllSync(task.class.TaskTypeDescriptor, { _id: taskType?.descriptor })
|
||||
$: states = (taskType?.statuses.map((p) => $statusStore.byId.get(p)).filter((p) => p !== undefined) as Status[]) ?? []
|
||||
|
||||
let tasksCounter: number = 0
|
||||
const tasksCounterQuery = createQuery()
|
||||
$: if (taskType !== undefined) {
|
||||
tasksCounterQuery.query(
|
||||
task.class.Task,
|
||||
{ kind: taskType._id },
|
||||
(res) => {
|
||||
tasksCounter = res.length
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function selectIcon (el: MouseEvent): void {
|
||||
const icons: Asset[] = [descriptor[0].icon]
|
||||
|
||||
showPopup(
|
||||
IconPicker,
|
||||
{ icon: taskType?.icon, color: taskType?.color, icons, showColor: false },
|
||||
el.target as HTMLElement,
|
||||
async (result) => {
|
||||
if (result !== undefined && result !== null) {
|
||||
if (result !== undefined && result !== null && taskType !== undefined) {
|
||||
await client.update(taskType, { color: result.color, icon: result.icon })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleAddStatus (): void {
|
||||
if (taskType === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const icons: Asset[] = []
|
||||
const attr = findStatusAttr(getClient().getHierarchy(), taskType.ofClass)
|
||||
$settingsStore = {
|
||||
@ -63,7 +108,7 @@
|
||||
taskType,
|
||||
_class: taskType.statusClass,
|
||||
category: task.statusCategory.Active,
|
||||
type: projectType,
|
||||
type: spaceType,
|
||||
ofAttribute: attr._id,
|
||||
icon: undefined,
|
||||
color: 0,
|
||||
@ -73,93 +118,120 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent-content__column-group mt-4">
|
||||
<div class="hulyComponent-content__header mb-6">
|
||||
<div class="flex-row-center gap-1-5">
|
||||
<TaskTypeKindEditor
|
||||
kind={taskType.kind}
|
||||
on:change={(evt) => {
|
||||
void client.diffUpdate(taskType, { kind: evt.detail })
|
||||
}}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={TaskTypeIcon}
|
||||
iconProps={{ value: taskType }}
|
||||
size={'large'}
|
||||
kind={'secondary'}
|
||||
on:click={selectIcon}
|
||||
/>
|
||||
{#if taskType !== undefined}
|
||||
<div class="hulyComponent-content__container columns">
|
||||
<div class="hulyComponent-content__column content">
|
||||
<Scroller align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}>
|
||||
<div class="hulyComponent-content gap">
|
||||
<div class="hulyComponent-content__column-group mt-4">
|
||||
<div class="hulyComponent-content__header mb-6">
|
||||
<div class="flex-row-center gap-1-5">
|
||||
<TaskTypeKindEditor
|
||||
kind={taskType.kind}
|
||||
on:change={(evt) => {
|
||||
if (taskType === undefined) {
|
||||
return
|
||||
}
|
||||
void client.diffUpdate(taskType, { kind: evt.detail })
|
||||
}}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={TaskTypeIcon}
|
||||
iconProps={{ value: taskType }}
|
||||
size={'large'}
|
||||
kind={'secondary'}
|
||||
on:click={selectIcon}
|
||||
/>
|
||||
</div>
|
||||
<ModernButton
|
||||
icon={IconSquareExpand}
|
||||
label={plugin.string.CountTasks}
|
||||
labelParams={{ count: tasksCounter }}
|
||||
disabled={tasksCounter === 0}
|
||||
kind={'tertiary'}
|
||||
size={'medium'}
|
||||
hasMenu
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AttributeEditor
|
||||
_class={task.class.TaskType}
|
||||
object={taskType}
|
||||
key="name"
|
||||
editKind={'modern-ghost-large'}
|
||||
/>
|
||||
|
||||
<div class="flex-row-center mt-4 ml-4 mr-4 gap-4">
|
||||
<div class="flex-no-shrink trans-title uppercase">
|
||||
<Label label={getEmbeddedLabel('Parent type restrictions')} />
|
||||
</div>
|
||||
{#if taskType.kind === 'subtask' || taskType.kind === 'both'}
|
||||
<TaskTypeRefEditor
|
||||
label={getEmbeddedLabel('Allowed parents')}
|
||||
value={taskType.allowedAsChildOf}
|
||||
types={taskTypes.filter((it) => it.kind === 'task' || it.kind === 'both')}
|
||||
onChange={(evt) => {
|
||||
if (taskType === undefined) {
|
||||
return
|
||||
}
|
||||
void client.diffUpdate(taskType, { allowedAsChildOf: evt })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hulyTableAttr-container">
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<Icon icon={task.icon.ManageTemplates} size={'small'} />
|
||||
<span><Label label={plugin.string.ProcessStates} /></span>
|
||||
<ButtonIcon kind={'primary'} icon={IconAdd} size={'small'} on:click={handleAddStatus} />
|
||||
</div>
|
||||
<StatesProjectEditor
|
||||
{taskType}
|
||||
type={spaceType}
|
||||
{states}
|
||||
on:delete={async (evt) => {
|
||||
if (taskType === undefined) {
|
||||
return
|
||||
}
|
||||
const index = taskType.statuses.findIndex((p) => p === evt.detail.state._id)
|
||||
taskType.statuses.splice(index, 1)
|
||||
await client.update(taskType, {
|
||||
statuses: taskType.statuses
|
||||
})
|
||||
await client.update(spaceType, {
|
||||
statuses: calculateStatuses(spaceType, $taskTypeStore, [
|
||||
{ taskTypeId: taskType._id, statuses: taskType.statuses }
|
||||
])
|
||||
})
|
||||
}}
|
||||
on:move={async (evt) => {
|
||||
if (taskType === undefined) {
|
||||
return
|
||||
}
|
||||
const index = taskType.statuses.findIndex((p) => p === evt.detail.stateID)
|
||||
const state = taskType.statuses.splice(index, 1)[0]
|
||||
|
||||
const statuses = [
|
||||
...taskType.statuses.slice(0, evt.detail.position),
|
||||
state,
|
||||
...taskType.statuses.slice(evt.detail.position)
|
||||
]
|
||||
await client.update(taskType, {
|
||||
statuses
|
||||
})
|
||||
|
||||
await client.update(spaceType, {
|
||||
statuses: calculateStatuses(spaceType, $taskTypeStore, [{ taskTypeId: taskType._id, statuses }])
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ClassAttributes ofClass={taskType.ofClass} _class={taskType.targetClass} showHierarchy />
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
<ModernButton
|
||||
icon={IconSquareExpand}
|
||||
label={plugin.string.CountTasks}
|
||||
labelParams={{ count: taskTypeCounter.get(taskType._id) ?? 0 }}
|
||||
disabled={taskTypeCounter.get(taskType._id) === undefined}
|
||||
kind={'tertiary'}
|
||||
size={'medium'}
|
||||
hasMenu
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AttributeEditor _class={task.class.TaskType} object={taskType} key="name" editKind={'modern-ghost-large'} />
|
||||
|
||||
<div class="flex-row-center mt-4 ml-4 mr-4 gap-4">
|
||||
<div class="flex-no-shrink trans-title uppercase">
|
||||
<Label label={getEmbeddedLabel('Parent type restrictions')} />
|
||||
</div>
|
||||
{#if taskType.kind === 'subtask' || taskType.kind === 'both'}
|
||||
<TaskTypeRefEditor
|
||||
label={getEmbeddedLabel('Allowed parents')}
|
||||
value={taskType.allowedAsChildOf}
|
||||
types={taskTypes.filter((it) => it.kind === 'task' || it.kind === 'both')}
|
||||
onChange={(evt) => {
|
||||
void client.diffUpdate(taskType, { allowedAsChildOf: evt })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hulyTableAttr-container">
|
||||
<div class="hulyTableAttr-header font-medium-12">
|
||||
<Icon icon={task.icon.ManageTemplates} size={'small'} />
|
||||
<span><Label label={plugin.string.ProcessStates} /></span>
|
||||
<ButtonIcon kind={'primary'} icon={IconAdd} size={'small'} on:click={handleAddStatus} />
|
||||
</div>
|
||||
<StatesProjectEditor
|
||||
{taskType}
|
||||
type={projectType}
|
||||
{states}
|
||||
on:delete={async (evt) => {
|
||||
const index = taskType.statuses.findIndex((p) => p === evt.detail.state._id)
|
||||
taskType.statuses.splice(index, 1)
|
||||
await client.update(taskType, {
|
||||
statuses: taskType.statuses
|
||||
})
|
||||
await client.update(projectType, {
|
||||
statuses: calculateStatuses(projectType, $taskTypeStore, [
|
||||
{ taskTypeId: taskType._id, statuses: taskType.statuses }
|
||||
])
|
||||
})
|
||||
}}
|
||||
on:move={async (evt) => {
|
||||
const index = taskType.statuses.findIndex((p) => p === evt.detail.stateID)
|
||||
const state = taskType.statuses.splice(index, 1)[0]
|
||||
|
||||
const statuses = [
|
||||
...taskType.statuses.slice(0, evt.detail.position),
|
||||
state,
|
||||
...taskType.statuses.slice(evt.detail.position)
|
||||
]
|
||||
await client.update(taskType, {
|
||||
statuses
|
||||
})
|
||||
|
||||
await client.update(projectType, {
|
||||
statuses: calculateStatuses(projectType, $taskTypeStore, [{ taskTypeId: taskType._id, statuses }])
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ClassAttributes ofClass={taskType.ofClass} _class={taskType.targetClass} showHierarchy />
|
||||
{/if}
|
||||
|
@ -53,9 +53,7 @@ import TaskPresenter from './components/TaskPresenter.svelte'
|
||||
import TemplatesIcon from './components/TemplatesIcon.svelte'
|
||||
import TypesView from './components/TypesView.svelte'
|
||||
import KanbanView from './components/kanban/KanbanView.svelte'
|
||||
import ProjectEditor from './components/projectTypes/ProjectEditor.svelte'
|
||||
import ProjectTypePresenter from './components/projectTypes/ProjectTypePresenter.svelte'
|
||||
import ProjectTypeSelector from './components/projectTypes/ProjectTypeSelector.svelte'
|
||||
import CreateStatePopup from './components/state/CreateStatePopup.svelte'
|
||||
import StateEditor from './components/state/StateEditor.svelte'
|
||||
import StateIconPresenter from './components/state/StateIconPresenter.svelte'
|
||||
@ -67,9 +65,13 @@ import TaskKindSelector from './components/taskTypes/TaskKindSelector.svelte'
|
||||
import TaskTypeClassPresenter from './components/taskTypes/TaskTypeClassPresenter.svelte'
|
||||
import TaskTypePresenter from './components/taskTypes/TaskTypePresenter.svelte'
|
||||
|
||||
import ManageProjects from './components/projectTypes/ManageProjects.svelte'
|
||||
import ManageProjectsContent from './components/projectTypes/ManageProjectsContent.svelte'
|
||||
import ManageProjectsTools from './components/projectTypes/ManageProjectsTools.svelte'
|
||||
import ProjectTypeSelector from './components/projectTypes/ProjectTypeSelector.svelte'
|
||||
import CreateProjectType from './components/projectTypes/CreateProjectType.svelte'
|
||||
import ProjectTypeGeneralSectionEditor from './components/projectTypes/ProjectTypeGeneralSectionEditor.svelte'
|
||||
import ProjectTypeTasksTypeSectionEditor from './components/projectTypes/ProjectTypeTasksTypeSectionEditor.svelte'
|
||||
import ProjectTypeAutomationsSectionEditor from './components/projectTypes/ProjectTypeAutomationsSectionEditor.svelte'
|
||||
import ProjectTypeCollectionsSectionEditor from './components/projectTypes/ProjectTypeCollectionsSectionEditor.svelte'
|
||||
import TaskTypeEditor from './components/taskTypes/TaskTypeEditor.svelte'
|
||||
|
||||
export { default as AssigneePresenter } from './components/AssigneePresenter.svelte'
|
||||
export { default as TypeSelector } from './components/TypeSelector.svelte'
|
||||
@ -112,8 +114,6 @@ export default async (): Promise<Resources> => ({
|
||||
StateEditor,
|
||||
StatusTableView,
|
||||
TaskHeader,
|
||||
ProjectEditor,
|
||||
ProjectTypeSelector,
|
||||
AssignedTasks,
|
||||
StateRefPresenter,
|
||||
DueDateEditor,
|
||||
@ -126,10 +126,14 @@ export default async (): Promise<Resources> => ({
|
||||
TaskTypePresenter,
|
||||
TaskTypeClassPresenter,
|
||||
ProjectTypeClassPresenter,
|
||||
ManageProjects,
|
||||
ManageProjectsTools,
|
||||
ManageProjectsContent,
|
||||
ProjectTypePresenter
|
||||
ProjectTypePresenter,
|
||||
ProjectTypeSelector,
|
||||
CreateProjectType,
|
||||
ProjectTypeGeneralSectionEditor,
|
||||
ProjectTypeTasksTypeSectionEditor,
|
||||
ProjectTypeAutomationsSectionEditor,
|
||||
ProjectTypeCollectionsSectionEditor,
|
||||
TaskTypeEditor
|
||||
},
|
||||
actionImpl: {
|
||||
EditStatuses: editStatuses,
|
||||
|
@ -91,7 +91,9 @@ export default mergeIds(taskId, task, {
|
||||
Color: '' as IntlString,
|
||||
|
||||
RenameStatus: '' as IntlString,
|
||||
UpdateTasksStatusRequest: '' as IntlString
|
||||
UpdateTasksStatusRequest: '' as IntlString,
|
||||
TaskTypes: '' as IntlString,
|
||||
Collections: '' as IntlString
|
||||
},
|
||||
status: {
|
||||
AssigneeRequired: '' as IntlString
|
||||
@ -101,7 +103,13 @@ export default mergeIds(taskId, task, {
|
||||
StatusFilter: '' as AnyComponent,
|
||||
TodoStatePresenter: '' as AnyComponent,
|
||||
AssignedTasks: '' as AnyComponent,
|
||||
DueDateEditor: '' as AnyComponent
|
||||
DueDateEditor: '' as AnyComponent,
|
||||
CreateProjectType: '' as AnyComponent,
|
||||
ProjectTypeGeneralSectionEditor: '' as AnyComponent,
|
||||
ProjectTypeTasksTypeSectionEditor: '' as AnyComponent,
|
||||
ProjectTypeAutomationsSectionEditor: '' as AnyComponent,
|
||||
ProjectTypeCollectionsSectionEditor: '' as AnyComponent,
|
||||
TaskTypeEditor: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
GetAllStates: '' as GetAllValuesFunc,
|
||||
|
@ -25,7 +25,10 @@ import {
|
||||
Space,
|
||||
Status,
|
||||
StatusCategory,
|
||||
Timestamp
|
||||
Timestamp,
|
||||
SpaceType,
|
||||
SpaceTypeDescriptor,
|
||||
TypedSpace
|
||||
} from '@hcengineering/core'
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
|
||||
@ -41,7 +44,7 @@ export interface DocWithRank extends Doc {
|
||||
rank: Rank
|
||||
}
|
||||
|
||||
export interface Project extends Space {
|
||||
export interface Project extends TypedSpace {
|
||||
type: Ref<ProjectType>
|
||||
}
|
||||
|
||||
@ -149,15 +152,16 @@ export interface TaskTypeClass extends Class<TaskType> {
|
||||
export interface ProjectTypeClass extends Class<ProjectType> {
|
||||
projectType: Ref<ProjectType>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Define a user customized project type.
|
||||
*/
|
||||
export interface ProjectType extends Space {
|
||||
shortDescription?: string
|
||||
export interface ProjectType extends SpaceType {
|
||||
descriptor: Ref<ProjectTypeDescriptor>
|
||||
tasks: Ref<TaskType>[]
|
||||
description: string
|
||||
|
||||
// Color and extra options per project type.
|
||||
// All statuses per project has same color.
|
||||
@ -173,14 +177,11 @@ export interface ProjectType extends Space {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ProjectTypeDescriptor extends Doc {
|
||||
name: IntlString
|
||||
description: IntlString
|
||||
icon: Asset
|
||||
editor?: AnyComponent
|
||||
export interface ProjectTypeDescriptor extends SpaceTypeDescriptor {
|
||||
allowedClassic?: boolean
|
||||
allowedTaskTypeDescriptors?: Ref<TaskTypeDescriptor>[] // if undefined we allow all possible
|
||||
baseClass: Ref<Class<Task>>
|
||||
baseClass: Ref<Class<Project>>
|
||||
editor?: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
@ -292,7 +293,6 @@ const task = plugin(taskId, {
|
||||
Lost: '' as Ref<StatusCategory>
|
||||
},
|
||||
component: {
|
||||
ProjectEditor: '' as AnyComponent,
|
||||
ProjectTypeSelector: '' as AnyComponent,
|
||||
CreateStatePopup: '' as AnyComponent
|
||||
},
|
||||
|
@ -168,7 +168,7 @@ export type TaskTypeWithFactory = Omit<Data<TaskType>, 'statuses' | 'parent' | '
|
||||
factory: Data<Status>[]
|
||||
} & Partial<Pick<TaskType, 'targetClass'>>
|
||||
|
||||
type ProjectData = Omit<Data<ProjectType>, 'statuses' | 'private' | 'members' | 'archived' | 'targetClass'>
|
||||
type ProjectData = Omit<Data<ProjectType>, 'statuses' | 'targetClass'>
|
||||
|
||||
async function createStates (
|
||||
client: TxOperations,
|
||||
@ -212,16 +212,14 @@ export async function createProjectType (
|
||||
const targetProjectClassId: Ref<Class<Doc>> = generateId()
|
||||
const tmpl = await client.createDoc(
|
||||
task.class.ProjectType,
|
||||
core.space.Space,
|
||||
core.space.Model,
|
||||
{
|
||||
description: data.description,
|
||||
shortDescription: data.shortDescription,
|
||||
descriptor: data.descriptor,
|
||||
roles: 0,
|
||||
tasks: _tasks,
|
||||
name: data.name,
|
||||
private: false,
|
||||
members: [],
|
||||
archived: false,
|
||||
statuses: calculateStatuses({ tasks: _tasks, statuses: [] }, tasksData, []),
|
||||
targetClass: targetProjectClassId,
|
||||
classic: data.classic
|
||||
@ -229,6 +227,7 @@ export async function createProjectType (
|
||||
_id
|
||||
)
|
||||
|
||||
// Mixin to hold custom fields of this project type
|
||||
await client.createDoc(
|
||||
core.class.Mixin,
|
||||
core.space.Model,
|
||||
@ -241,6 +240,7 @@ export async function createProjectType (
|
||||
targetProjectClassId
|
||||
)
|
||||
|
||||
// TODO: not needed ???
|
||||
await client.createMixin(targetProjectClassId, core.class.Mixin, core.space.Model, task.mixin.ProjectTypeClass, {
|
||||
projectType: _id
|
||||
})
|
||||
@ -344,7 +344,7 @@ async function createTaskTypes (
|
||||
projectType: _id
|
||||
})
|
||||
}
|
||||
await client.createDoc(task.class.TaskType, _id, tdata as Data<TaskType>, taskId)
|
||||
await client.createDoc(task.class.TaskType, core.space.Model, tdata as Data<TaskType>, taskId)
|
||||
tasksData.set(taskId, tdata as Data<TaskType>)
|
||||
_tasks.push(taskId)
|
||||
}
|
||||
|
@ -286,7 +286,8 @@
|
||||
"MapRelatedIssues": "Configure Related issue default projects",
|
||||
"DefaultIssueStatus": "Default issue status",
|
||||
"IssueStatus": "Status",
|
||||
"Extensions": "Extensions"
|
||||
"Extensions": "Extensions",
|
||||
"RoleLabel": "Role: {role}"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -286,7 +286,8 @@
|
||||
"MapRelatedIssues": "Настроить проекты по умолчанию для связанных задач",
|
||||
"DefaultIssueStatus": "Статус по умолчанию",
|
||||
"IssueStatus": "Cтатус",
|
||||
"Extensions": "Дополнительно"
|
||||
"Extensions": "Дополнительно",
|
||||
"RoleLabel": "Роль: {role}"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -13,9 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { AccountArrayEditor, AssigneeBox } from '@hcengineering/contact-resources'
|
||||
import core, { Account, Data, DocumentUpdate, Ref, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import core, {
|
||||
Account,
|
||||
Data,
|
||||
DocumentUpdate,
|
||||
RolesAssignment,
|
||||
Ref,
|
||||
Role,
|
||||
SortingOrder,
|
||||
generateId,
|
||||
getCurrentAccount
|
||||
} from '@hcengineering/core'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import task, { ProjectType } from '@hcengineering/task'
|
||||
@ -25,11 +36,9 @@
|
||||
Button,
|
||||
Component,
|
||||
EditBox,
|
||||
IconEdit,
|
||||
IconWithEmoji,
|
||||
Label,
|
||||
Toggle,
|
||||
eventToHTMLElement,
|
||||
getColorNumberByText,
|
||||
getPlatformColorDef,
|
||||
getPlatformColorForTextDef,
|
||||
@ -38,14 +47,12 @@
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { IconPicker } from '@hcengineering/view-resources'
|
||||
import { typeStore, taskTypeStore } from '@hcengineering/task-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import StatusSelector from '../issues/StatusSelector.svelte'
|
||||
import ChangeIdentity from './ChangeIdentity.svelte'
|
||||
import { typeStore, taskTypeStore } from '@hcengineering/task-resources'
|
||||
|
||||
export let project: Project | undefined = undefined
|
||||
|
||||
export let namePlaceholder: string = ''
|
||||
export let descriptionPlaceholder: string = ''
|
||||
|
||||
@ -65,6 +72,7 @@
|
||||
let projectsIdentifiers = new Set<string>()
|
||||
let isSaving = false
|
||||
let defaultStatus: Ref<IssueStatus> | undefined = project?.defaultIssueStatus
|
||||
let rolesAssignment: RolesAssignment | undefined
|
||||
|
||||
let changeIdentityRef: HTMLElement
|
||||
|
||||
@ -99,8 +107,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getRolesAssignment (): RolesAssignment {
|
||||
if (project === undefined || typeType?.targetClass === undefined || roles === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const asMixin = hierarchy.as(project, typeType?.targetClass)
|
||||
|
||||
return roles.reduce<RolesAssignment>((prev, { _id }) => {
|
||||
prev[_id] = (asMixin as any)[_id]
|
||||
|
||||
return prev
|
||||
}, {})
|
||||
}
|
||||
|
||||
async function updateProject (): Promise<void> {
|
||||
if (!project) {
|
||||
if (!project || typeType?.targetClass === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -147,11 +169,20 @@
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
if (rolesAssignment && !deepEqual(rolesAssignment, getRolesAssignment())) {
|
||||
await client.updateMixin(
|
||||
project._id,
|
||||
tracker.class.Project,
|
||||
core.space.Space,
|
||||
typeType.targetClass,
|
||||
rolesAssignment
|
||||
)
|
||||
}
|
||||
|
||||
close()
|
||||
}
|
||||
|
||||
let typeId: Ref<ProjectType> | undefined = project?.type
|
||||
|
||||
$: typeType = typeId !== undefined ? $typeStore.get(typeId) : undefined
|
||||
|
||||
$: if (defaultStatus === undefined && typeType !== undefined) {
|
||||
@ -171,8 +202,14 @@
|
||||
const succeeded = await ops.commit()
|
||||
|
||||
if (succeeded) {
|
||||
// Add vacancy mixin
|
||||
await client.createMixin(projectId, tracker.class.Project, core.space.Space, typeType.targetClass, {})
|
||||
// Add space type's mixin with roles assignments
|
||||
await client.createMixin(
|
||||
projectId,
|
||||
tracker.class.Project,
|
||||
core.space.Space,
|
||||
typeType.targetClass,
|
||||
rolesAssignment ?? {}
|
||||
)
|
||||
|
||||
close(projectId)
|
||||
} else {
|
||||
@ -207,6 +244,52 @@
|
||||
}
|
||||
|
||||
$: identifier = identifier.toLocaleUpperCase().replaceAll('-', '_').replaceAll(' ', '_').substring(0, 5)
|
||||
|
||||
let roles: Role[] = []
|
||||
const rolesQuery = createQuery()
|
||||
$: if (typeType !== undefined) {
|
||||
rolesQuery.query(
|
||||
core.class.Role,
|
||||
{ attachedTo: typeType._id },
|
||||
(res) => {
|
||||
roles = res
|
||||
|
||||
if (rolesAssignment === undefined && typeType !== undefined) {
|
||||
rolesAssignment = getRolesAssignment()
|
||||
}
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
name: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
rolesQuery.unsubscribe()
|
||||
}
|
||||
|
||||
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)
|
||||
const removedMembersSet = new Set(members.filter((m) => !newMembersSet.has(m)))
|
||||
|
||||
if (removedMembersSet.size > 0 && rolesAssignment !== undefined) {
|
||||
for (const [key, value] of Object.entries(rolesAssignment)) {
|
||||
rolesAssignment[key as Ref<Role>] =
|
||||
value !== undefined ? value.filter((m) => !removedMembersSet.has(m)) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
members = newMembers
|
||||
}
|
||||
|
||||
function handleRoleAssignmentChanged (roleId: Ref<Role>, newMembers: Ref<Account>[]): void {
|
||||
if (rolesAssignment === undefined) {
|
||||
rolesAssignment = {}
|
||||
}
|
||||
|
||||
rolesAssignment[roleId] = newMembers
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@ -326,19 +409,6 @@
|
||||
<Toggle bind:on={isPrivate} disabled={!isPrivate && members.length === 0} />
|
||||
</div>
|
||||
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={tracker.string.Members} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
value={members}
|
||||
label={tracker.string.Members}
|
||||
onChange={(refs) => (members = refs)}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={tracker.string.DefaultAssignee} />
|
||||
@ -369,6 +439,38 @@
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={tracker.string.Members} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
value={members}
|
||||
label={tracker.string.Members}
|
||||
onChange={handleMembersChanged}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#each roles as role}
|
||||
<div class="antiGrid-row">
|
||||
<div class="antiGrid-row__header">
|
||||
<Label label={tracker.string.RoleLabel} params={{ role: role.name }} />
|
||||
</div>
|
||||
<AccountArrayEditor
|
||||
value={rolesAssignment?.[role._id] ?? []}
|
||||
label={tracker.string.Members}
|
||||
includeItems={members}
|
||||
readonly={members.length === 0}
|
||||
onChange={(refs) => {
|
||||
handleRoleAssignmentChanged(role._id, refs)
|
||||
}}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
@ -299,7 +299,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
UnsetParent: '' as IntlString,
|
||||
PreviousAssigned: '' as IntlString,
|
||||
EditRelatedTargets: '' as IntlString,
|
||||
RelatedIssueTargetDescription: '' as IntlString
|
||||
RelatedIssueTargetDescription: '' as IntlString,
|
||||
RoleLabel: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
NopeComponent: '' as AnyComponent,
|
||||
|
@ -0,0 +1,59 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 core, { type Ref, type SpaceType, type SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { type DropdownTextItem, ButtonKind, ButtonSize, DropdownLabels } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
export let descriptors: Ref<SpaceTypeDescriptor>[]
|
||||
export let type: Ref<SpaceType> | undefined = undefined
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let focusIndex = -1
|
||||
export let disabled: boolean = false
|
||||
|
||||
let types: SpaceType[] = []
|
||||
const typesQ = createQuery()
|
||||
const query = {
|
||||
descriptor: { $in: descriptors }
|
||||
}
|
||||
$: typesQ.query(core.class.SpaceType, query, (result) => {
|
||||
types = result
|
||||
})
|
||||
|
||||
let items: DropdownTextItem[] = []
|
||||
$: items = types.map((x) => ({ id: x._id, label: x.name }))
|
||||
|
||||
let selectedItem: string | undefined = type
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: {
|
||||
type = selectedItem === undefined ? undefined : (selectedItem as Ref<SpaceType>)
|
||||
dispatch('change', type)
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownLabels
|
||||
{focusIndex}
|
||||
{items}
|
||||
{kind}
|
||||
{size}
|
||||
{disabled}
|
||||
icon={view.icon.Setting}
|
||||
bind:selected={selectedItem}
|
||||
label={core.string.SpaceType}
|
||||
/>
|
@ -79,6 +79,7 @@ import SearchSelector from './components/SearchSelector.svelte'
|
||||
import SpaceHeader from './components/SpaceHeader.svelte'
|
||||
import SpacePresenter from './components/SpacePresenter.svelte'
|
||||
import SpaceRefPresenter from './components/SpaceRefPresenter.svelte'
|
||||
import SpaceTypeSelector from './components/SpaceTypeSelector.svelte'
|
||||
import StatusPresenter from './components/status/StatusPresenter.svelte'
|
||||
import StatusRefPresenter from './components/status/StatusRefPresenter.svelte'
|
||||
import StringEditor from './components/StringEditor.svelte'
|
||||
@ -117,6 +118,7 @@ import {
|
||||
import { IndexedDocumentPreview } from '@hcengineering/presentation'
|
||||
import { AggregationMiddleware, AnalyticsMiddleware } from './middleware'
|
||||
import { showEmptyGroups } from './viewOptions'
|
||||
import { canDeleteObject } 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'
|
||||
@ -133,11 +135,13 @@ export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.s
|
||||
export { default as ContextMenu } from './components/Menu.svelte'
|
||||
export { default as NavLink } from './components/navigator/NavLink.svelte'
|
||||
export { default as ObjectBox } from './components/ObjectBox.svelte'
|
||||
export { default as ObjectBoxPopup } from './components/ObjectBoxPopup.svelte'
|
||||
export { default as ObjectPresenter } from './components/ObjectPresenter.svelte'
|
||||
export { default as ObjectSearchBox } from './components/ObjectSearchBox.svelte'
|
||||
export { default as ParentsNavigator } from './components/ParentsNavigator.svelte'
|
||||
export { default as StatusPresenter } from './components/status/StatusPresenter.svelte'
|
||||
export { default as StatusRefPresenter } from './components/status/StatusRefPresenter.svelte'
|
||||
export { default as SpaceTypeSelector } from './components/SpaceTypeSelector.svelte'
|
||||
export { default as TableBrowser } from './components/TableBrowser.svelte'
|
||||
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
||||
export { default as ViewletSelector } from './components/ViewletSelector.svelte'
|
||||
@ -255,6 +259,7 @@ export default async (): Promise<Resources> => ({
|
||||
DividerPresenter,
|
||||
IndexedDocumentPreview,
|
||||
SpaceRefPresenter,
|
||||
SpaceTypeSelector,
|
||||
EnumArrayEditor,
|
||||
EnumPresenter,
|
||||
StatusPresenter,
|
||||
@ -293,6 +298,7 @@ export default async (): Promise<Resources> => ({
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
CreateDocMiddleware: AggregationMiddleware.create,
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
AnalyticsMiddleware: AnalyticsMiddleware.create
|
||||
AnalyticsMiddleware: AnalyticsMiddleware.create,
|
||||
CanDeleteObject: canDeleteObject
|
||||
}
|
||||
})
|
||||
|
@ -29,7 +29,8 @@ export default mergeIds(viewId, view, {
|
||||
TimestampFilter: '' as AnyComponent,
|
||||
FilterTypePopup: '' as AnyComponent,
|
||||
ProxyPresenter: '' as AnyComponent,
|
||||
ArrayEditor: '' as AnyComponent
|
||||
ArrayEditor: '' as AnyComponent,
|
||||
SpaceTypeSelector: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Contains: '' as IntlString,
|
||||
|
@ -47,7 +47,9 @@ import core, {
|
||||
type TxMixin,
|
||||
type TxOperations,
|
||||
type TxUpdateDoc,
|
||||
type TypeAny
|
||||
type TypeAny,
|
||||
type TypedSpace,
|
||||
type Permission
|
||||
} from '@hcengineering/core'
|
||||
import { type Restrictions } from '@hcengineering/guest'
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
@ -60,7 +62,8 @@ import {
|
||||
hasResource,
|
||||
type KeyedAttribute,
|
||||
getFiltredKeys,
|
||||
isAdminUser
|
||||
isAdminUser,
|
||||
createQuery
|
||||
} from '@hcengineering/presentation'
|
||||
import {
|
||||
ErrorPresenter,
|
||||
@ -1327,3 +1330,47 @@ async function getAttrEditor (key: KeyedAttribute, hierarchy: Hierarchy): Promis
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
type PermissionsBySpace = Record<Ref<Space>, Set<Ref<Permission>>>
|
||||
interface PermissionsStore {
|
||||
ps: PermissionsBySpace
|
||||
whitelist: Set<Ref<Space>>
|
||||
}
|
||||
|
||||
export const permissionsStore = writable<PermissionsStore>({
|
||||
ps: {},
|
||||
whitelist: new Set()
|
||||
})
|
||||
const permissionsQuery = createQuery(true)
|
||||
|
||||
permissionsQuery.query(core.class.Space, {}, (res) => {
|
||||
const whitelistedSpaces = new Set<Ref<Space>>()
|
||||
const permissionsBySpace: PermissionsBySpace = {}
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const me = getCurrentAccount()
|
||||
|
||||
for (const s of res) {
|
||||
if (hierarchy.isDerived(s._class, core.class.TypedSpace)) {
|
||||
const type = client.getModel().findAllSync(core.class.SpaceType, { _id: (s as TypedSpace).type })[0]
|
||||
const mixin = type?.targetClass
|
||||
|
||||
if (mixin === undefined) {
|
||||
permissionsBySpace[s._id] = new Set()
|
||||
continue
|
||||
}
|
||||
|
||||
const asMixin = hierarchy.as(s, mixin)
|
||||
const roles = client.getModel().findAllSync(core.class.Role, { attachedTo: type._id })
|
||||
const myRoles = roles.filter((r) => (asMixin as any)[r._id].includes(me._id))
|
||||
permissionsBySpace[s._id] = new Set(myRoles.flatMap((r) => r.permissions))
|
||||
} else {
|
||||
whitelistedSpaces.add(s._id)
|
||||
}
|
||||
}
|
||||
|
||||
permissionsStore.set({
|
||||
ps: permissionsBySpace,
|
||||
whitelist: whitelistedSpaces
|
||||
})
|
||||
})
|
||||
|
42
plugins/view-resources/src/visibilityTester.ts
Normal file
42
plugins/view-resources/src/visibilityTester.ts
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// 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 core, { checkPermission, type Space, type Doc, type TypedSpace } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
function isTypedSpace (space: Space): space is TypedSpace {
|
||||
return getClient().getHierarchy().isDerived(space._class, core.class.TypedSpace)
|
||||
}
|
||||
|
||||
export async function canDeleteObject (doc?: Doc | Doc[]): Promise<boolean> {
|
||||
if (doc === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const targets = Array.isArray(doc) ? doc : [doc]
|
||||
// Note: allow deleting objects in NOT typed spaces for now
|
||||
const targetSpaces = (await client.findAll(core.class.Space, { _id: { $in: targets.map((t) => t.space) } })).filter(
|
||||
isTypedSpace
|
||||
)
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
Array.from(new Set(targetSpaces.map((t) => t._id))).map(
|
||||
async (s) => await checkPermission(client, core.permission.DeleteObject, s)
|
||||
)
|
||||
)
|
||||
).every((r) => r)
|
||||
}
|
@ -32,7 +32,8 @@ import {
|
||||
ModifiedMiddleware,
|
||||
PrivateMiddleware,
|
||||
QueryJoinMiddleware,
|
||||
SpaceSecurityMiddleware
|
||||
SpaceSecurityMiddleware,
|
||||
SpacePermissionsMiddleware
|
||||
} from '@hcengineering/middleware'
|
||||
import { createMongoAdapter, createMongoTxAdapter } from '@hcengineering/mongo'
|
||||
import { OpenAIEmbeddingsStage, openAIId, openAIPluginImpl } from '@hcengineering/openai'
|
||||
@ -229,6 +230,7 @@ export function start (
|
||||
ModifiedMiddleware.create,
|
||||
PrivateMiddleware.create,
|
||||
SpaceSecurityMiddleware.create,
|
||||
SpacePermissionsMiddleware.create,
|
||||
ConfigurationMiddleware.create,
|
||||
QueryJoinMiddleware.create // Should be last one
|
||||
]
|
||||
|
@ -19,3 +19,4 @@ export * from './modified'
|
||||
export * from './private'
|
||||
export * from './queryJoin'
|
||||
export * from './spaceSecurity'
|
||||
export * from './spacePermissions'
|
||||
|
346
server/middleware/src/spacePermissions.ts
Normal file
346
server/middleware/src/spacePermissions.ts
Normal file
@ -0,0 +1,346 @@
|
||||
//
|
||||
// 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 core, {
|
||||
Account,
|
||||
AttachedDoc,
|
||||
Class,
|
||||
Doc,
|
||||
MeasureContext,
|
||||
Permission,
|
||||
Ref,
|
||||
Role,
|
||||
RolesAssignment,
|
||||
ServerStorage,
|
||||
Space,
|
||||
SpaceType,
|
||||
Tx,
|
||||
TxApplyIf,
|
||||
TxCUD,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc,
|
||||
TypedSpace
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import { BroadcastFunc, Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core'
|
||||
|
||||
import { BaseMiddleware } from './base'
|
||||
import { getUser } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class SpacePermissionsMiddleware extends BaseMiddleware implements Middleware {
|
||||
private spaceMeasureCtx!: MeasureContext
|
||||
private whitelistSpaces = new Set<Ref<Space>>()
|
||||
private assignmentBySpace: Record<Ref<Space>, RolesAssignment> = {}
|
||||
private permissionsBySpace: Record<Ref<Space>, Record<Ref<Account>, Set<Ref<Permission>>>> = {}
|
||||
private typeBySpace: Record<Ref<Space>, Ref<SpaceType>> = {}
|
||||
|
||||
static async create (
|
||||
ctx: MeasureContext,
|
||||
broadcast: BroadcastFunc,
|
||||
storage: ServerStorage,
|
||||
next?: Middleware
|
||||
): Promise<SpacePermissionsMiddleware> {
|
||||
const res = new SpacePermissionsMiddleware(storage, next)
|
||||
res.spaceMeasureCtx = ctx.newChild('space permisisons', {})
|
||||
await res.init(res.spaceMeasureCtx)
|
||||
return res
|
||||
}
|
||||
|
||||
private async init (ctx: MeasureContext): Promise<void> {
|
||||
const spaces: Space[] = await this.storage.findAll(ctx, core.class.Space, {})
|
||||
|
||||
for (const space of spaces) {
|
||||
if (this.isTypedSpace(space)) {
|
||||
await this.addRestrictedSpace(space)
|
||||
}
|
||||
}
|
||||
|
||||
this.whitelistSpaces = new Set(spaces.filter((s) => !this.isTypedSpaceClass(s._class)).map((p) => p._id))
|
||||
}
|
||||
|
||||
private async getRoles (spaceTypeId: Ref<SpaceType>): Promise<Role[]> {
|
||||
return await this.storage.modelDb.findAll(core.class.Role, { attachedTo: spaceTypeId })
|
||||
}
|
||||
|
||||
private async setPermissions (spaceId: Ref<Space>, roles: Role[], assignment: RolesAssignment): Promise<void> {
|
||||
for (const role of roles) {
|
||||
const roleMembers: Ref<Account>[] = assignment[role._id] ?? []
|
||||
|
||||
for (const member of roleMembers) {
|
||||
if (this.permissionsBySpace[spaceId][member] === undefined) {
|
||||
this.permissionsBySpace[spaceId][member] = new Set()
|
||||
}
|
||||
|
||||
for (const permission of role.permissions) {
|
||||
this.permissionsBySpace[spaceId][member].add(permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async addRestrictedSpace (space: TypedSpace): Promise<void> {
|
||||
this.permissionsBySpace[space._id] = {}
|
||||
|
||||
const spaceType = await this.storage.modelDb.findOne(core.class.SpaceType, { _id: space.type })
|
||||
|
||||
if (spaceType === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.typeBySpace[space._id] = space.type
|
||||
|
||||
const asMixin: RolesAssignment = this.storage.hierarchy.as(
|
||||
space,
|
||||
spaceType.targetClass
|
||||
) as unknown as RolesAssignment
|
||||
this.assignmentBySpace[space._id] = asMixin
|
||||
|
||||
await this.setPermissions(space._id, await this.getRoles(spaceType._id), asMixin)
|
||||
}
|
||||
|
||||
private isTypedSpaceClass (_class: Ref<Class<Space>>): boolean {
|
||||
const h = this.storage.hierarchy
|
||||
|
||||
return h.isDerived(_class, core.class.TypedSpace)
|
||||
}
|
||||
|
||||
private isTypedSpace (space: Space): space is TypedSpace {
|
||||
return this.isTypedSpaceClass(space._class)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* Throws if the required permission is missing in the space for the given context
|
||||
*/
|
||||
private async needPermission (ctx: SessionContext, space: Ref<TypedSpace>, id: Ref<Permission>): Promise<void> {
|
||||
const account = await getUser(this.storage, ctx)
|
||||
const permissions = this.permissionsBySpace[space]?.[account._id] ?? new Set()
|
||||
|
||||
if (!permissions.has(id)) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCreate (tx: TxCUD<Space>): Promise<void> {
|
||||
const createTx = tx as TxCreateDoc<Space>
|
||||
if (!this.storage.hierarchy.isDerived(createTx.objectClass, core.class.Space)) return
|
||||
if (this.isTypedSpaceClass(createTx.objectClass)) {
|
||||
const res = TxProcessor.buildDoc2Doc<TypedSpace>([createTx])
|
||||
if (res !== undefined) {
|
||||
await this.addRestrictedSpace(res)
|
||||
}
|
||||
} else {
|
||||
this.whitelistSpaces.add(createTx.objectId)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMixin (tx: TxCUD<Space>): Promise<void> {
|
||||
if (!this.isTypedSpaceClass(tx.objectClass)) {
|
||||
return
|
||||
}
|
||||
|
||||
const spaceId = tx.objectId
|
||||
const spaceTypeId = this.typeBySpace[spaceId]
|
||||
|
||||
if (spaceTypeId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const spaceType = await this.storage.modelDb.findOne(core.class.SpaceType, { _id: spaceTypeId })
|
||||
|
||||
if (spaceType === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const mixinDoc = tx as TxMixin<Space, Space>
|
||||
|
||||
if (mixinDoc.mixin !== spaceType.targetClass) {
|
||||
return
|
||||
}
|
||||
|
||||
// Note: currently the whole assignment is always included into the mixin update
|
||||
// so we can just rebuild the permissions
|
||||
const assignment: RolesAssignment = mixinDoc.attributes as RolesAssignment
|
||||
this.assignmentBySpace[spaceId] = assignment
|
||||
|
||||
this.permissionsBySpace[tx.objectId] = {}
|
||||
await this.setPermissions(spaceId, await this.getRoles(spaceType._id), assignment)
|
||||
}
|
||||
|
||||
private handleRemove (tx: TxCUD<Space>): void {
|
||||
const removeTx = tx as TxRemoveDoc<Space>
|
||||
if (!this.storage.hierarchy.isDerived(removeTx.objectClass, core.class.Space)) return
|
||||
if (removeTx._class !== core.class.TxCreateDoc) return
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.permissionsBySpace[tx.objectId]
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.assignmentBySpace[tx.objectId]
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.typeBySpace[tx.objectId]
|
||||
|
||||
this.whitelistSpaces.delete(tx.objectId)
|
||||
}
|
||||
|
||||
private isSpaceTxCUD (tx: TxCUD<Doc>): tx is TxCUD<Space> {
|
||||
return this.storage.hierarchy.isDerived(tx.objectClass, core.class.Space)
|
||||
}
|
||||
|
||||
private isTxCollectionCUD (tx: TxCUD<Doc>): tx is TxCollectionCUD<Doc, AttachedDoc> {
|
||||
return this.storage.hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)
|
||||
}
|
||||
|
||||
private isRoleTxCUD (tx: TxCUD<Doc>): tx is TxCUD<Role> {
|
||||
return this.storage.hierarchy.isDerived(tx.objectClass, core.class.Role)
|
||||
}
|
||||
|
||||
private async handlePermissionsUpdatesFromRoleTx (ctx: SessionContext, tx: TxCUD<Doc>): Promise<void> {
|
||||
if (!this.isTxCollectionCUD(tx)) {
|
||||
return
|
||||
}
|
||||
|
||||
const h = this.storage.hierarchy
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
if (!h.isDerived(actualTx._class, core.class.TxCUD)) {
|
||||
return
|
||||
}
|
||||
|
||||
const actualCudTx = actualTx as TxCUD<Doc>
|
||||
|
||||
if (!this.isRoleTxCUD(actualCudTx)) {
|
||||
return
|
||||
}
|
||||
|
||||
// We are only interested in updates of the existing roles because:
|
||||
// When role is created it always has empty set of permissions
|
||||
// And it's not currently possible to delete a role
|
||||
|
||||
if (actualCudTx._class !== core.class.TxUpdateDoc) {
|
||||
return
|
||||
}
|
||||
|
||||
const updateTx = actualCudTx as TxUpdateDoc<Role>
|
||||
|
||||
if (updateTx.operations.permissions === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find affected spaces
|
||||
const targetSpaceTypeId = tx.objectId
|
||||
const affectedSpacesIds = Object.entries(this.typeBySpace)
|
||||
.filter(([, typeId]) => typeId === targetSpaceTypeId)
|
||||
.map(([spaceId]) => spaceId) as Ref<TypedSpace>[]
|
||||
|
||||
for (const spaceId of affectedSpacesIds) {
|
||||
const spaceTypeId = this.typeBySpace[spaceId]
|
||||
|
||||
if (spaceTypeId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const assignment: RolesAssignment = this.assignmentBySpace[spaceId]
|
||||
const roles = await this.getRoles(spaceTypeId)
|
||||
const targetRole = roles.find((r) => r._id === updateTx.objectId)
|
||||
|
||||
if (targetRole === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
targetRole.permissions = updateTx.operations.permissions
|
||||
|
||||
this.permissionsBySpace[spaceId] = {}
|
||||
await this.setPermissions(spaceId, roles, assignment)
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePermissionsUpdatesFromTx (ctx: SessionContext, tx: TxCUD<Doc>): Promise<void> {
|
||||
if (this.isSpaceTxCUD(tx)) {
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
await this.handleCreate(tx)
|
||||
// } else if (tx._class === core.class.TxUpdateDoc) {
|
||||
// Roles assignment in spaces are managed through the space type mixin
|
||||
// so nothing to handle here
|
||||
} else if (tx._class === core.class.TxMixin) {
|
||||
await this.handleMixin(tx)
|
||||
} else if (tx._class === core.class.TxRemoveDoc) {
|
||||
this.handleRemove(tx)
|
||||
}
|
||||
}
|
||||
|
||||
await this.handlePermissionsUpdatesFromRoleTx(ctx, tx)
|
||||
}
|
||||
|
||||
private async processPermissionsUpdatesFromTx (ctx: SessionContext, tx: Tx): Promise<void> {
|
||||
const h = this.storage.hierarchy
|
||||
if (!h.isDerived(tx._class, core.class.TxCUD)) {
|
||||
return
|
||||
}
|
||||
|
||||
const cudTx = tx as TxCUD<Doc>
|
||||
await this.handlePermissionsUpdatesFromTx(ctx, cudTx)
|
||||
}
|
||||
|
||||
async tx (ctx: SessionContext, tx: Tx): Promise<TxMiddlewareResult> {
|
||||
await this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||
await this.checkPermissions(ctx, tx)
|
||||
const res = await this.provideTx(ctx, tx)
|
||||
for (const tx of res[1]) {
|
||||
await this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
handleBroadcast (tx: Tx[], targets?: string[]): Tx[] {
|
||||
return this.provideHandleBroadcast(tx, targets)
|
||||
}
|
||||
|
||||
protected async checkPermissions (ctx: SessionContext, tx: Tx): Promise<void> {
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
const applyTx = tx as TxApplyIf
|
||||
|
||||
await Promise.all(applyTx.txes.map((t) => this.checkPermissions(ctx, t)))
|
||||
return
|
||||
}
|
||||
|
||||
if (tx._class === core.class.TxCollectionCUD) {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
|
||||
await this.checkPermissions(ctx, actualTx)
|
||||
}
|
||||
|
||||
const cudTx = tx as TxCUD<Doc>
|
||||
const h = this.storage.hierarchy
|
||||
const isSpace = h.isDerived(cudTx.objectClass, core.class.Space)
|
||||
// NOTE: in assumption that we want to control permissions for space itself on that space level
|
||||
// and not on the system's spaces space level for now
|
||||
const targetSpaceId = (isSpace ? cudTx.objectId : cudTx.objectSpace) as Ref<Space>
|
||||
|
||||
if (this.whitelistSpaces.has(targetSpaceId)) {
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: move this checking logic later to be defined in some server plugins?
|
||||
// so they can contribute checks into the middleware for their custom permissions?
|
||||
if (tx._class === core.class.TxRemoveDoc) {
|
||||
await this.needPermission(ctx, targetSpaceId as Ref<TypedSpace>, core.permission.DeleteObject)
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,10 @@ export class CommonPage {
|
||||
await page.locator('div[class$="opup"] span[class*="label"]', { hasText: point }).click()
|
||||
}
|
||||
|
||||
async checkDropdownHasNo (page: Page, item: string): Promise<void> {
|
||||
await expect(page.locator('div.selectPopup span[class^="lines"]', { hasText: item })).not.toBeVisible()
|
||||
}
|
||||
|
||||
async fillToDropdown (page: Page, input: string): Promise<void> {
|
||||
await page.locator('div.popup input.search').fill(input)
|
||||
await page.locator('div.popup button#channel-ok').click()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user