Backport plugin permissions in the workspace (#9966)

* Apply patch

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>

* Fix merge errors

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>

* Fix drive header

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>

* Fix formatting

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>

* Fix tests

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>

---------

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
Anton Alexeyev
2025-09-30 09:55:39 +07:00
committed by GitHub
parent 7681ccb899
commit 764d963885
87 changed files with 801 additions and 427 deletions
@@ -12,6 +12,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.ReviewDocumentPermission,
scope: 'space',
description: documents.string.ReviewDocumentDescription
},
documents.permission.ReviewDocument
@@ -22,6 +23,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.ApproveDocumentPermission,
scope: 'space',
description: documents.string.ApproveDocumentDescription
},
documents.permission.ApproveDocument
@@ -32,6 +34,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.ArchiveDocumentPermission,
scope: 'space',
description: documents.string.ArchiveDocumentDescription
},
documents.permission.ArchiveDocument
@@ -42,6 +45,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.CoAuthorDocumentPermission,
scope: 'space',
description: documents.string.CoAuthorDocumentDescription
},
documents.permission.CoAuthorDocument
@@ -52,6 +56,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.CreateDocumentPermission,
scope: 'space',
description: documents.string.CreateDocumentDescription
},
documents.permission.CreateDocument
@@ -62,6 +67,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.UpdateDocumentOwnerPermission,
scope: 'space',
description: documents.string.UpdateDocumentOwnerDescription
},
documents.permission.UpdateDocumentOwner
@@ -72,6 +78,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.CreateDocumentCategoryPermission,
scope: 'space',
description: documents.string.CreateDocumentCategoryDescription
},
documents.permission.CreateDocumentCategory
@@ -82,6 +89,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.UpdateDocumentCategoryPermission,
scope: 'space',
description: documents.string.UpdateDocumentCategoryDescription
},
documents.permission.UpdateDocumentCategory
@@ -92,6 +100,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: documents.string.DeleteDocumentCategoryPermission,
scope: 'space',
description: documents.string.DeleteDocumentCategoryDescription
},
documents.permission.DeleteDocumentCategory
+8 -20
View File
@@ -23,6 +23,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.CreateObject,
scope: 'space',
description: core.string.CreateObjectDescription
},
core.permission.CreateObject
@@ -33,6 +34,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.UpdateObject,
scope: 'space',
description: core.string.UpdateObjectDescription
},
core.permission.UpdateObject
@@ -43,6 +45,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.DeleteObject,
scope: 'space',
description: core.string.DeleteObjectDescription
},
core.permission.DeleteObject
@@ -53,36 +56,20 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.ForbidDeleteObject,
txClass: core.class.TxRemoveDoc,
forbid: true,
scope: 'space',
description: core.string.ForbidDeleteObjectDescription
},
core.permission.ForbidDeleteObject
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: core.string.UpdateObject,
description: core.string.UpdateObjectDescription
},
core.permission.UpdateObject
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: core.string.DeleteObject,
description: core.string.DeleteObjectDescription
},
core.permission.DeleteObject
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: core.string.UpdateSpace,
scope: 'space',
description: core.string.UpdateSpaceDescription
},
core.permission.UpdateSpace
@@ -93,6 +80,7 @@ export function definePermissions (builder: Builder): void {
core.space.Model,
{
label: core.string.ArchiveSpace,
scope: 'space',
description: core.string.ArchiveSpaceDescription
},
core.permission.ArchiveSpace
+7 -1
View File
@@ -29,7 +29,9 @@ import {
type SpaceTypeDescriptor,
type TypedSpace,
type AccountUuid,
type TxAccessLevel
type TxAccessLevel,
type Tx,
type Doc
} from '@hcengineering/core'
import {
ArrOf,
@@ -155,6 +157,10 @@ export class TRole extends TAttachedDoc implements Role {
@UX(core.string.Permission)
export class TPermission extends TDoc implements Permission {
label!: IntlString
txClass?: Ref<Class<Tx>>
forbid?: boolean
objectClass?: Ref<Class<Doc<Space>>>
scope?: 'space' | 'workspace'
description?: IntlString
icon?: Asset
}
+2
View File
@@ -63,6 +63,7 @@ import { type Asset, getEmbeddedLabel } from '@hcengineering/platform'
import tags from '@hcengineering/tags'
import time, { type ToDo, type Todoable } from '@hcengineering/time'
import document from './plugin'
import { definePermissions } from './permissions'
export { documentId } from '@hcengineering/document'
@@ -550,6 +551,7 @@ export function createModel (builder: Builder): void {
defineDocument(builder)
defineApplication(builder)
definePermissions(builder)
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_DOCUMENT,
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import document from '@hcengineering/document'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: document.string.ForbidCreateTeamspacePermission,
scope: 'workspace',
txClass: core.class.TxCreateDoc,
objectClass: document.class.Teamspace,
forbid: true,
description: document.string.ForbidCreateTeamspacePermissionDescription
},
document.permission.ForbidCreateTeamspace
)
}
+2
View File
@@ -63,6 +63,7 @@ import workbench from '@hcengineering/model-workbench'
import { getEmbeddedLabel } from '@hcengineering/platform'
import drive from './plugin'
import { definePermissions } from './permissions'
export { driveId } from '@hcengineering/drive'
export { driveOperation } from './migration'
@@ -790,4 +791,5 @@ export function createModel (builder: Builder): void {
defineFile(builder)
defineFileVersion(builder)
defineApplication(builder)
definePermissions(builder)
}
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import drive from '@hcengineering/drive'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: drive.string.ForbidCreateDrivePermission,
scope: 'workspace',
txClass: core.class.TxCreateDoc,
objectClass: drive.class.Drive,
forbid: true,
description: drive.string.ForbidCreateDrivePermissionDescription
},
drive.permission.ForbidCreateDrive
)
}
+2
View File
@@ -32,6 +32,7 @@ import { type ViewOptionsModel } from '@hcengineering/view'
import lead from './plugin'
import { defineSpaceType } from './spaceType'
import { definePermissions } from './permissions'
import { TCustomer, TFunnel, TLead } from './types'
export { leadId } from '@hcengineering/lead'
@@ -660,4 +661,5 @@ export function createModel (builder: Builder): void {
})
defineSpaceType(builder)
definePermissions(builder)
}
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import lead from '@hcengineering/lead'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: lead.string.ForbidCreateFunnelPermission,
scope: 'workspace',
txClass: core.class.TxCreateDoc,
objectClass: lead.class.Funnel,
forbid: true,
description: lead.string.ForbidCreateFunnelPermissionDescription
},
lead.permission.ForbidCreateFunnel
)
}
+23 -17
View File
@@ -37,6 +37,7 @@ import { type KeyBinding, type ViewOptionModel, type ViewOptionsModel } from '@h
import recruit from './plugin'
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
import { defineSpaceType } from './spaceType'
import { definePermissions } from './permissions'
import { TApplicant, TApplicantMatch, TCandidate, TOpinion, TReview, TVacancy, TVacancyList } from './types'
export { recruitId } from '@hcengineering/recruit'
@@ -1019,24 +1020,28 @@ export function createModel (builder: Builder): void {
},
override: [recruit.action.CreateGlobalApplication]
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {
component: recruit.component.CreateCandidate,
element: 'top'
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: recruit.component.CreateCandidate,
element: 'top'
},
label: recruit.string.CreateTalent,
icon: recruit.icon.Create,
keyBinding: ['keyC'],
input: 'none',
category: recruit.category.Recruit,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser'],
application: recruit.app.Recruit,
group: 'create'
}
},
label: recruit.string.CreateTalent,
icon: recruit.icon.Create,
keyBinding: ['keyC'],
input: 'none',
category: recruit.category.Recruit,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser'],
application: recruit.app.Recruit,
group: 'create'
}
})
recruit.action.CreateTalent
)
createAction(builder, {
action: view.actionImpl.ShowPopup,
@@ -1596,4 +1601,5 @@ export function createModel (builder: Builder): void {
)
defineSpaceType(builder)
definePermissions(builder)
}
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import recruit from '@hcengineering/recruit'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: recruit.string.ForbidCreateVacancyPermission,
scope: 'workspace',
txClass: core.class.TxCreateDoc,
objectClass: recruit.class.Vacancy,
forbid: true,
description: recruit.string.ForbidCreateVacancyPermissionDescription
},
recruit.permission.ForbidCreateVacancy
)
}
+2
View File
@@ -32,6 +32,7 @@ import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import { createActions as defineActions } from './actions'
import tracker from './plugin'
import { definePresenters } from './presenters'
import { definePermissions } from './permissions'
import {
DOMAIN_TRACKER,
TClassicProjectTypeData,
@@ -709,6 +710,7 @@ export function createModel (builder: Builder): void {
]
})
definePermissions(builder)
defineSpaceType(builder)
}
+19
View File
@@ -0,0 +1,19 @@
import type { Builder } from '@hcengineering/model'
import core from '@hcengineering/core'
import tracker from '@hcengineering/tracker'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
label: tracker.string.ForbidCreateProjectPermission,
txClass: core.class.TxCreateDoc,
objectClass: tracker.class.Project,
forbid: true,
scope: 'workspace',
description: tracker.string.ForbidCreateProjectPermissionDescription
},
tracker.permission.ForbidCreateProject
)
}
+8
View File
@@ -354,18 +354,22 @@ function defineTraining (builder: Builder): void {
definePermission(builder, training.permission.ChangeSomeoneElsesTrainingOwner, {
label: training.string.Permission_ChangeSomeoneElsesTrainingOwner,
scope: 'space',
description: training.string.Permission_ChangeSomeoneElsesTrainingOwner_Description
})
definePermission(builder, training.permission.CreateTraining, {
label: training.string.Permission_CreateTraining,
scope: 'space',
description: training.string.Permission_CreateTraining_Description
})
definePermission(builder, training.permission.ViewSomeoneElsesTrainingOverview, {
label: training.string.Permission_ViewSomeoneElsesTrainingOverview,
scope: 'space',
description: training.string.Permission_ViewSomeoneElsesTrainingOverview_Description
})
definePermission(builder, training.permission.ViewSomeoneElsesTrainingQuestions, {
label: training.string.Permission_ViewSomeoneElsesTrainingQuestions,
scope: 'space',
description: training.string.Permission_ViewSomeoneElsesTrainingQuestions_Description
})
}
@@ -491,14 +495,17 @@ function defineTrainingRequest (builder: Builder): void {
definePermission(builder, training.permission.ChangeSomeoneElsesSentRequestOwner, {
label: training.string.Permission_ChangeSomeoneElsesSentRequestOwner,
scope: 'space',
description: training.string.Permission_ChangeSomeoneElsesSentRequestOwner_Description
})
definePermission(builder, training.permission.CreateRequestOnSomeoneElsesTraining, {
label: training.string.Permission_CreateRequestOnSomeoneElsesTraining,
scope: 'space',
description: training.string.Permission_CreateRequestOnSomeoneElsesTraining_Description
})
definePermission(builder, training.permission.ViewSomeoneElsesSentRequest, {
label: training.string.Permission_ViewSomeoneElsesSentRequest,
scope: 'space',
description: training.string.Permission_ViewSomeoneElsesSentRequest_Description
})
})()
@@ -753,6 +760,7 @@ function defineTrainingAttempt (builder: Builder): void {
definePermission(builder, training.permission.ViewSomeoneElsesTraineesResults, {
label: training.string.Permission_ViewSomeoneElsesTraineesResults,
scope: 'space',
description: training.string.Permission_ViewSomeoneElsesTraineesResults_Description
})
}
+1 -1
View File
@@ -69,4 +69,4 @@
"PersonId": "Osoba",
"AccountId": "Účet"
}
}
}
+1 -1
View File
@@ -69,4 +69,4 @@
"PersonId": "Id de personne",
"AccountId": "Compte"
}
}
}
+5
View File
@@ -17,6 +17,7 @@
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import type { DocumentQuery } from './storage'
import { type WorkspaceDataId, type WorkspaceUuid } from './utils'
import { Tx } from '.'
/**
* @public
@@ -529,6 +530,10 @@ export type RolesAssignment = Record<Ref<Role>, AccountUuid[] | undefined>
*/
export interface Permission extends Doc {
label: IntlString
txClass?: Ref<Class<Tx>>
forbid?: boolean
objectClass?: Ref<Class<Doc>>
scope?: 'space' | 'workspace'
description?: IntlString
icon?: Asset
}
@@ -0,0 +1,130 @@
<!--
// Copyright © 2025 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 { HeaderButtonAction, SelectPopupValueType } from '../types'
import { checkPermission, Client, getCurrentAccount, hasAccountRole, TxOperations } from '@hcengineering/core'
import { ButtonWithDropdown, Button, Loading, IconAdd, IconDropdown } from '../index'
export let mainActionId: number | string | null = null
export let loading = false
export let client: TxOperations & Client
export let actions: HeaderButtonAction[] = []
export let visibleActions: (string | number | null)[] = []
let allowedActions: HeaderButtonAction[] = []
let items: HeaderButtonAction[] = []
let mainAction: HeaderButtonAction | undefined = undefined
$: filterVisibleActions(allowedActions, visibleActions)
$: filterAllowedActions(actions).catch(() => {})
function filterVisibleActions (allowed: HeaderButtonAction[], visible: (string | number | null)[]): void {
items = allowed.filter((action) => visible.includes(action.id))
mainAction = items.find((a) => a.id === mainActionId)
if (mainAction === undefined && items.length > 0) {
mainAction = items[0]
}
}
async function filterAllowedActions (actions: HeaderButtonAction[]): Promise<SelectPopupValueType[]> {
const result: HeaderButtonAction[] = []
for (const action of actions) {
if (await isActionAllowed(action)) {
result.push(action)
}
action.keyBinding = await action.keyBindingPromise
}
allowedActions = result
return result
}
async function isActionAllowed (action: HeaderButtonAction): Promise<boolean> {
if (action.accountRole === undefined && action.permissions === undefined) return true
if (action.accountRole !== undefined && hasAccountRole(getCurrentAccount(), action.accountRole)) return true
if (action.permissions !== undefined) {
for (const permission of action.permissions) {
if (await checkPermission(client, permission.id, permission.space)) return true
}
}
return false
}
</script>
{#if mainAction !== undefined}
{#if loading}
<Loading shrink />
{:else}
<div class="antiNav-subheader">
{#if items.length === 1}
<Button
icon={IconAdd}
justify="left"
kind="primary"
label={mainAction.label}
width="100%"
on:click={mainAction.callback}
showTooltip={{
direction: 'bottom',
label: mainAction.label,
keys: mainAction.keyBinding
}}
>
<div slot="content" class="draft-circle-container">
{#if mainAction.draft === true}
<div class="draft-circle" />
{/if}
</div>
</Button>
{:else}
<ButtonWithDropdown
icon={IconAdd}
justify={'left'}
kind={'primary'}
label={mainAction.label}
dropdownItems={items}
dropdownIcon={IconDropdown}
on:dropdown-selected={(ev) => {
items.find((a) => a.id === ev.detail)?.callback()
}}
on:click={mainAction.callback}
mainButtonId={mainAction.id !== null ? String(mainAction.id).replaceAll(':', '-') : undefined}
showTooltipMain={{
direction: 'bottom',
label: mainAction.label,
keys: mainAction.keyBinding
}}
>
<div slot="content" class="draft-circle-container">
{#if mainAction.draft === true}
<div class="draft-circle" />
{/if}
</div>
</ButtonWithDropdown>
{/if}
</div>
{/if}
{/if}
<style lang="scss">
.draft-circle-container {
margin-left: auto;
padding-right: 12px;
}
.draft-circle {
height: 6px;
width: 6px;
background-color: var(--primary-bg-color);
border-radius: 50%;
}
</style>
+1
View File
@@ -56,6 +56,7 @@ export { getCurrentLocation, locationToUrl, navigate, location, setLocationStora
export { default as EditBox } from './components/EditBox.svelte'
export { default as Label } from './components/Label.svelte'
export { default as Button } from './components/Button.svelte'
export { default as HeaderButton } from './components/HeaderButton.svelte'
export { default as ButtonWithDropdown } from './components/ButtonWithDropdown.svelte'
export { default as ButtonGroup } from './components/ButtonGroup.svelte'
export { default as FilterButton } from './components/FilterButton.svelte'
+16 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { Blob, Ref, Timestamp } from '@hcengineering/core'
import type { AccountRole, Blob, Permission, Ref, Timestamp, TypedSpace } from '@hcengineering/core'
import type {
Asset,
IntlString,
@@ -519,6 +519,21 @@ export interface SelectPopupValueType {
}
}
/**
* @public
*/
export interface HeaderButtonAction extends SelectPopupValueType {
callback: () => void
keyBindingPromise?: Promise<string[] | undefined>
keyBinding?: string[] | undefined
draft?: boolean
accountRole?: AccountRole
permissions?: Array<{
id: Ref<Permission>
space: Ref<TypedSpace>
}>
}
/**
* @public
*/
+4 -2
View File
@@ -58,6 +58,8 @@
"Icon": "Ikona",
"Color": "Barva",
"RoleLabel": "Role: {role}"
"RoleLabel": "Role: {role}",
"ForbidCreateTeamspacePermission": "Zakázat vytvoření týmového prostoru",
"ForbidCreateTeamspacePermissionDescription": "Zakazuje uživatelům vytvářet nové týmové prostory"
}
}
}
+4 -2
View File
@@ -51,6 +51,8 @@
"ConfigDescription": "Erweiterung für kollaborative Dokumentbearbeitung",
"Icon": "Symbol",
"Color": "Farbe",
"RoleLabel": "Rolle: {role}"
"RoleLabel": "Rolle: {role}",
"ForbidCreateTeamspacePermission": "Arbeitsbereichserstellung verbieten",
"ForbidCreateTeamspacePermissionDescription": "Verbietet Benutzern das Erstellen neuer Arbeitsbereiche"
}
}
}
+4 -2
View File
@@ -58,6 +58,8 @@
"Icon": "Icon",
"Color": "Color",
"RoleLabel": "Role: {role}"
"RoleLabel": "Role: {role}",
"ForbidCreateTeamspacePermission": "Forbid create teamspace",
"ForbidCreateTeamspacePermissionDescription": "Forbid users creating new teamspaces"
}
}
}
+4 -2
View File
@@ -51,6 +51,8 @@
"ConfigDescription": "Extensión para edición colaborativo de documentos",
"Icon": "Icono",
"Color": "Color",
"RoleLabel": "Role: {role}"
"RoleLabel": "Role: {role}",
"ForbidCreateTeamspacePermission": "Prohibir crear espacio de trabajo",
"ForbidCreateTeamspacePermissionDescription": "Prohíbe a los usuarios crear nuevos espacios de trabajo"
}
}
}
+4 -2
View File
@@ -51,6 +51,8 @@
"ConfigDescription": "Extension pour l'édition collaborative de documents",
"Icon": "Icône",
"Color": "Couleur",
"RoleLabel": "Rôle : {role}"
"RoleLabel": "Rôle : {role}",
"ForbidCreateTeamspacePermission": "Interdire la création d'espace d'équipe",
"ForbidCreateTeamspacePermissionDescription": "Interdit aux utilisateurs de créer de nouveaux espaces d'équipe"
}
}
}
+3 -1
View File
@@ -51,6 +51,8 @@
"ConfigDescription": "Estensione per la modifica collaborativa dei documenti",
"Icon": "Icona",
"Color": "Colore",
"RoleLabel": "Ruolo: {role}"
"RoleLabel": "Ruolo: {role}",
"ForbidCreateTeamspacePermission": "Vieta creazione teamspace",
"ForbidCreateTeamspacePermissionDescription": "Vieta agli utenti di creare nuovi teamspaces"
}
}
+3 -1
View File
@@ -51,6 +51,8 @@
"ConfigDescription": "共同ドキュメント編集のための拡張機能",
"Icon": "アイコン",
"Color": "色",
"RoleLabel": "ロール: {role}"
"RoleLabel": "ロール: {role}",
"ForbidCreateTeamspacePermission": "チームスペースの作成禁止",
"ForbidCreateTeamspacePermissionDescription": "ユーザーが新しいチームスペースを作成することを禁止します"
}
}
+4 -2
View File
@@ -51,6 +51,8 @@
"ConfigDescription": "Extensão para edição colaborativa de documentos",
"Icon": "Ícone",
"Color": "Cor",
"RoleLabel": "Papel: {role}"
"RoleLabel": "Papel: {role}",
"ForbidCreateTeamspacePermission": "Proibir criação de espaço de trabalho",
"ForbidCreateTeamspacePermissionDescription": "Proíbe os utilizadores de criar novos espaços de trabalho"
}
}
}
+4 -2
View File
@@ -58,6 +58,8 @@
"Icon": "Иконка",
"Color": "Цвет",
"RoleLabel": "Роль: {role}"
"RoleLabel": "Роль: {role}",
"ForbidCreateTeamspacePermission": "Запретить создание пространства",
"ForbidCreateTeamspacePermissionDescription": "Запрещает пользователям создавать новые пространства"
}
}
}
+3 -1
View File
@@ -62,6 +62,8 @@
"ReassignToDoConfirm": "您要更改待办事项被指派人吗?待办事项将从当前被指派人的计划中移除。",
"Icon": "图标",
"Color": "颜色",
"RoleLabel": "角色:{role}"
"RoleLabel": "角色:{role}",
"ForbidCreateTeamspacePermission": "禁止创建团队空间",
"ForbidCreateTeamspacePermissionDescription": "禁止用户创建新团队空间"
}
}
@@ -13,17 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { AccountRole, Ref, Space, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import core, { AccountRole, Ref, Space, getCurrentAccount } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
Button,
ButtonWithDropdown,
IconAdd,
IconDropdown,
Loading,
SelectPopupValueType,
showPopup
} from '@hcengineering/ui'
import { HeaderButton, showPopup } from '@hcengineering/ui'
import { openDoc } from '@hcengineering/view-resources'
import { Analytics } from '@hcengineering/analytics'
import { DocumentEvents } from '@hcengineering/document'
@@ -70,51 +62,37 @@
showPopup(CreateTeamspace, {}, 'top')
}
async function dropdownItemSelected (res?: SelectPopupValueType['id']): Promise<void> {
if (res === document.string.CreateDocument) {
await newDocument()
} else if (res === document.string.CreateTeamspace) {
await newTeamspace()
let mainActionId: string | undefined = undefined
let visibleActions: string[] = []
function updateActions (teamspace: boolean): void {
mainActionId = document.string.CreateDocument
if (teamspace) {
visibleActions = [document.string.CreateTeamspace, document.string.CreateDocument]
} else {
visibleActions = [document.string.CreateTeamspace]
}
}
const dropdownItems = hasAccountRole(myAcc, AccountRole.User)
? [
{ id: document.string.CreateDocument, label: document.string.CreateDocument },
{ id: document.string.CreateTeamspace, label: document.string.CreateTeamspace }
]
: [{ id: document.string.CreateDocument, label: document.string.CreateDocument }]
$: updateActions(hasTeamspace)
</script>
{#if loading}
<Loading shrink />
{:else}
<div class="antiNav-subheader">
{#if hasTeamspace}
<ButtonWithDropdown
icon={IconAdd}
justify={'left'}
kind={'primary'}
label={document.string.CreateDocument}
on:click={newDocument}
mainButtonId={'new-document'}
dropdownIcon={IconDropdown}
{dropdownItems}
on:dropdown-selected={(ev) => {
void dropdownItemSelected(ev.detail)
}}
/>
{:else}
<Button
id={'new-teamspace'}
icon={IconAdd}
label={document.string.CreateTeamspace}
justify={'left'}
width={'100%'}
kind={'primary'}
gap={'large'}
on:click={newTeamspace}
/>
{/if}
</div>
{/if}
<HeaderButton
{loading}
{client}
{mainActionId}
{visibleActions}
actions={[
{
id: document.string.CreateTeamspace,
label: document.string.CreateTeamspace,
accountRole: AccountRole.User,
callback: newTeamspace
},
{
id: document.string.CreateDocument,
label: document.string.CreateDocument,
accountRole: AccountRole.User,
callback: newDocument
}
]}
/>
+7 -2
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import type { Class, Doc, Mixin, Ref, SpaceType, SpaceTypeDescriptor } from '@hcengineering/core'
import type { Class, Doc, Mixin, Ref, SpaceType, SpaceTypeDescriptor, Permission } from '@hcengineering/core'
import { NotificationGroup, NotificationType } from '@hcengineering/notification'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
@@ -69,7 +69,9 @@ export const documentPlugin = plugin(documentId, {
string: {
ConfigLabel: '' as IntlString,
CreateDocument: '' as IntlString,
Documents: '' as IntlString
Documents: '' as IntlString,
ForbidCreateTeamspacePermission: '' as IntlString,
ForbidCreateTeamspacePermissionDescription: '' as IntlString
},
ids: {
NoParent: '' as Ref<Document>,
@@ -81,6 +83,9 @@ export const documentPlugin = plugin(documentId, {
},
spaceType: {
DefaultTeamspaceType: '' as Ref<SpaceType>
},
permission: {
ForbidCreateTeamspace: '' as Ref<Permission>
}
})
+4 -2
View File
@@ -27,6 +27,8 @@
"Rename": "Přejmenovat",
"Restore": "Obnovit",
"RoleLabel": "Role",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "Zakázat vytvoření disku",
"ForbidCreateDrivePermissionDescription": "Zakazuje uživatelům vytvářet nové disky"
}
}
}
+3 -1
View File
@@ -27,6 +27,8 @@
"Rename": "Umbenennen",
"Restore": "Wiederherstellen",
"RoleLabel": "Rolle",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "Laufwerkserstellung verbieten",
"ForbidCreateDrivePermissionDescription": "Verbietet Benutzern die Erstellung neuer Laufwerke"
}
}
+3 -1
View File
@@ -27,6 +27,8 @@
"Rename": "Rename",
"Restore": "Restore",
"RoleLabel": "Role",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "Forbid create drive",
"ForbidCreateDrivePermissionDescription": "Forbid users creating new drives"
}
}
+4 -2
View File
@@ -27,6 +27,8 @@
"Rename": "Renombrar",
"Restore": "Restaurar",
"RoleLabel": "Rol",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "Prohibir crear unidad",
"ForbidCreateDrivePermissionDescription": "Prohíbe a los usuarios crear nuevas unidades"
}
}
}
+4 -2
View File
@@ -27,6 +27,8 @@
"Rename": "Renommer",
"Restore": "Restaurer",
"RoleLabel": "Rôle",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "Interdire la création de disque",
"ForbidCreateDrivePermissionDescription": "Interdit aux utilisateurs de créer de nouveaux disques"
}
}
}
+3 -1
View File
@@ -27,6 +27,8 @@
"Rename": "Rinomina",
"Restore": "Ripristina",
"RoleLabel": "Ruolo",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "Vieta creazione drive",
"ForbidCreateDrivePermissionDescription": "Vieta agli utenti di creare nuovi drive"
}
}
+3 -1
View File
@@ -27,6 +27,8 @@
"Rename": "名前を変更",
"Restore": "復元",
"RoleLabel": "ロール",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "ドライブの作成禁止",
"ForbidCreateDrivePermissionDescription": "ユーザーが新しいドライブを作成することを禁止します"
}
}
+4 -2
View File
@@ -27,6 +27,8 @@
"Rename": "Renomear",
"Restore": "Restaurar",
"RoleLabel": "Papel",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "Proibir criação de unidade",
"ForbidCreateDrivePermissionDescription": "Proíbe os utilizadores de criar novas unidades"
}
}
}
+3 -1
View File
@@ -27,6 +27,8 @@
"Rename": "Переименовать",
"Restore": "Восстановить",
"RoleLabel": "Роль",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "Запретить создание диска",
"ForbidCreateDrivePermissionDescription": "Запрещает пользователям создавать новые диски"
}
}
+3 -1
View File
@@ -27,6 +27,8 @@
"Rename": "重命名",
"Restore": "恢复",
"RoleLabel": "角色",
"Root": "/"
"Root": "/",
"ForbidCreateDrivePermission": "禁止创建磁盘",
"ForbidCreateDrivePermissionDescription": "禁止用户创建新磁盘"
}
}
@@ -13,44 +13,66 @@
// limitations under the License.
-->
<script lang="ts">
import { AccountRole, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import { AccountRole, Ref, getCurrentAccount } from '@hcengineering/core'
import { type Drive } from '@hcengineering/drive'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Button, ButtonWithDropdown, IconAdd, IconDropdown, Loading, SelectPopupValueType } from '@hcengineering/ui'
import { FileUploadOptions, getUploadHandlers, UploadHandler } from '@hcengineering/uploader'
import { HeaderButton, HeaderButtonAction } from '@hcengineering/ui'
import { getUploadHandlers } from '@hcengineering/uploader'
import drive from '../plugin'
import { getFolderIdFromFragment } from '../navigation'
import {
showCreateDrivePopup,
showCreateFolderPopup,
uploadFilesToDrivePopup,
getUploadOptionsByFragment
} from '../utils'
import { showCreateDrivePopup, showCreateFolderPopup, getUploadOptionsByFragment } from '../utils'
import { onMount } from 'svelte'
export let currentSpace: Ref<Drive> | undefined
export let currentFragment: string | undefined
const basicActions: HeaderButtonAction[] = [
{
id: drive.string.CreateDrive,
label: drive.string.CreateDrive,
icon: drive.icon.Drive,
accountRole: AccountRole.User,
callback: handleCreateDrive
},
{
id: drive.string.CreateFolder,
label: drive.string.CreateFolder,
icon: drive.icon.Folder,
callback: handleCreateFolder
}
]
let uploadActions: HeaderButtonAction[] = []
let allActions: HeaderButtonAction[] = []
const myAcc = getCurrentAccount()
const client = getClient()
const query = createQuery()
const actionWithExtensionMap = new Map<string, UploadHandler>()
onMount(() => {
const handlers = getUploadHandlers(client)
const newUploadActions: HeaderButtonAction[] = []
for (const handler of handlers) {
dropdownItems.push({ id: handler._id, label: handler.label, icon: handler.icon })
const uploadHandler = async (opts: FileUploadOptions): Promise<void> => {
const uploadHandler = async (): Promise<void> => {
if (currentSpace === undefined) return
const fn = await getResource(handler.handler)
const opts = await getUploadOptionsByFragment(currentSpace, currentFragment ?? '')
await fn(opts)
}
actionWithExtensionMap.set(handler._id, uploadHandler)
newUploadActions.push({
id: handler.label,
label: handler.label,
icon: handler.icon,
callback: () => {
void uploadHandler()
}
})
}
uploadActions = newUploadActions
})
let loading = true
let hasDrive = false
query.query(
drive.class.Drive,
@@ -63,74 +85,25 @@
$: parent = getFolderIdFromFragment(currentFragment ?? '') ?? drive.ids.Root
async function handleDropdownItemSelected (res?: SelectPopupValueType['id']): Promise<void> {
if (res === drive.string.CreateDrive) {
await handleCreateDrive()
} else if (res === drive.string.CreateFolder) {
await handleCreateFolder()
} else if (typeof res === 'string' && currentSpace !== undefined) {
const opts = await getUploadOptionsByFragment(currentSpace, currentFragment ?? '')
const uploadFn = actionWithExtensionMap.get(res)
if (uploadFn === undefined) {
return
}
await uploadFn(opts)
function handleCreateDrive (): void {
void showCreateDrivePopup()
}
function handleCreateFolder (): void {
void showCreateFolderPopup(currentSpace, parent, true)
}
let visibleActions: (string | number | null)[] = []
function updateActions (hasSpace: boolean, uploadActions: HeaderButtonAction[]): void {
allActions = [...basicActions, ...uploadActions]
if (hasSpace) {
visibleActions = allActions.map((a) => a.id)
} else {
visibleActions = [drive.string.CreateDrive]
}
}
async function handleCreateDrive (): Promise<void> {
await showCreateDrivePopup()
}
async function handleCreateFolder (): Promise<void> {
await showCreateFolderPopup(currentSpace, parent, true)
}
async function handleUploadFile (): Promise<void> {
if (currentSpace !== undefined) {
await uploadFilesToDrivePopup(currentSpace, parent)
}
}
const dropdownItems: SelectPopupValueType[] = hasAccountRole(myAcc, AccountRole.User)
? [
{ id: drive.string.CreateDrive, label: drive.string.CreateDrive, icon: drive.icon.Drive },
{ id: drive.string.CreateFolder, label: drive.string.CreateFolder, icon: drive.icon.Folder }
]
: [{ id: drive.string.CreateFolder, label: drive.string.CreateFolder, icon: drive.icon.Folder }]
loading = false
$: updateActions(hasDrive, uploadActions)
</script>
{#if loading}
<Loading shrink />
{:else}
<div class="antiNav-subheader">
{#if hasDrive}
<ButtonWithDropdown
icon={IconAdd}
justify={'left'}
kind={'primary'}
label={drive.string.UploadFile}
mainButtonId={'new-document'}
dropdownIcon={IconDropdown}
{dropdownItems}
disabled={currentSpace === undefined}
on:click={handleUploadFile}
on:dropdown-selected={(ev) => {
void handleDropdownItemSelected(ev.detail)
}}
/>
{:else}
<Button
icon={IconAdd}
label={drive.string.CreateDrive}
justify={'left'}
width={'100%'}
kind={'primary'}
gap={'large'}
on:click={handleCreateDrive}
/>
{/if}
</div>
{/if}
<HeaderButton loading={false} {client} mainActionId={uploadActions[0]?.id} {visibleActions} actions={allActions} />
+7 -2
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import type { Class, Doc, Mixin, Ref, SpaceType, SpaceTypeDescriptor, Type } from '@hcengineering/core'
import type { Class, Doc, Mixin, Ref, SpaceType, SpaceTypeDescriptor, Type, Permission } from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource as PlatformResource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Location, ResolvedLocation } from '@hcengineering/ui/src/types'
@@ -64,13 +64,18 @@ export const drivePlugin = plugin(driveId, {
File: '' as IntlString,
FileVersion: '' as IntlString,
Folder: '' as IntlString,
Resource: '' as IntlString
Resource: '' as IntlString,
ForbidCreateDrivePermission: '' as IntlString,
ForbidCreateDrivePermissionDescription: '' as IntlString
},
descriptor: {
DriveType: '' as Ref<SpaceTypeDescriptor>
},
spaceType: {
DefaultDrive: '' as Ref<SpaceType>
},
permission: {
ForbidCreateDrive: '' as Ref<Permission>
}
})
+4 -2
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "Rozšíření pro řízení vztahů se zákazníky",
"EditFunnel": "Upravit trychtýř",
"FunnelMembers": "Členové",
"RoleLabel": "Role: {role}"
"RoleLabel": "Role: {role}",
"ForbidCreateFunnelPermission": "Zakázat vytvoření trychtýře",
"ForbidCreateFunnelPermissionDescription": "Zakazuje uživatelům vytvářet nové trychtýře"
}
}
}
+4 -2
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "Erweiterung für Customer Relationship Management",
"EditFunnel": "Trichter bearbeiten",
"FunnelMembers": "Mitglieder",
"RoleLabel": "Rolle: {role}"
"RoleLabel": "Rolle: {role}",
"ForbidCreateFunnelPermission": "Trichtererstellung verbieten",
"ForbidCreateFunnelPermissionDescription": "Verbietet Benutzern das Erstellen neuer Trichter"
}
}
}
+4 -2
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "Extension for Customer relation management",
"EditFunnel": "Edit Funnel",
"FunnelMembers": "Members",
"RoleLabel": "Role: {role}"
"RoleLabel": "Role: {role}",
"ForbidCreateFunnelPermission": "Forbid create funnel",
"ForbidCreateFunnelPermissionDescription": "Forbid users creating new funnels"
}
}
}
+4 -2
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "Extensión para la gestión de las relaciones con los Clientes",
"EditFunnel": "Editar Embudo",
"FunnelMembers": "Miembros",
"RoleLabel": "Role: {role}"
"RoleLabel": "Role: {role}",
"ForbidCreateFunnelPermission": "Prohibir crear embudo",
"ForbidCreateFunnelPermissionDescription": "Prohíbe a los usuarios crear nuevos embudos"
}
}
}
+4 -2
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "Extension pour la gestion de la relation client",
"EditFunnel": "Modifier le pipeline",
"FunnelMembers": "Membres",
"RoleLabel": "Rôle : {role}"
"RoleLabel": "Rôle : {role}",
"ForbidCreateFunnelPermission": "Interdire la création de pipeline",
"ForbidCreateFunnelPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux pipelines"
}
}
}
+3 -1
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "Estensione per la gestione delle relazioni con i clienti",
"EditFunnel": "Modifica Funnel",
"FunnelMembers": "Membri",
"RoleLabel": "Ruolo: {role}"
"RoleLabel": "Ruolo: {role}",
"ForbidCreateFunnelPermission": "Vieta creazione funnel",
"ForbidCreateFunnelPermissionDescription": "Vieta agli utenti di creare nuovi funnel"
}
}
+3 -1
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "顧客関係管理のための拡張機能",
"EditFunnel": "ファネルを編集",
"FunnelMembers": "メンバー",
"RoleLabel": "ロール: {role}"
"RoleLabel": "ロール: {role}",
"ForbidCreateFunnelPermission": "ファネルの作成禁止",
"ForbidCreateFunnelPermissionDescription": "ユーザーが新しいファネルを作成することを禁止します"
}
}
+4 -2
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "Extensão para gestão de relacionamento com o Cliente",
"EditFunnel": "Editar Funil",
"FunnelMembers": "Membros",
"RoleLabel": "Cargo: {role}"
"RoleLabel": "Cargo: {role}",
"ForbidCreateFunnelPermission": "Proibir criação de funil",
"ForbidCreateFunnelPermissionDescription": "Proíbe os utilizadores de criar novos funis"
}
}
}
+4 -2
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "Расширение по работе с клиентами",
"EditFunnel": "Редактировать воронку",
"FunnelMembers": "Участники",
"RoleLabel": "Роль: {role}"
"RoleLabel": "Роль: {role}",
"ForbidCreateFunnelPermission": "Запретить создание воронки",
"ForbidCreateFunnelPermissionDescription": "Запрещает пользователям создавать новые воронки"
}
}
}
+3 -1
View File
@@ -34,6 +34,8 @@
"ConfigDescription": "用于客户关系管理的扩展",
"EditFunnel": "编辑漏斗",
"FunnelMembers": "成员",
"RoleLabel": "角色:{role}"
"RoleLabel": "角色:{role}",
"ForbidCreateFunnelPermission": "禁止创建漏斗",
"ForbidCreateFunnelPermissionDescription": "禁止用户创建新漏斗"
}
}
@@ -13,23 +13,29 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, showPopup, IconAdd } from '@hcengineering/ui'
import { showPopup, HeaderButton } from '@hcengineering/ui'
import lead from '../plugin'
import CreateCustomer from './CreateCustomer.svelte'
import { AccountRole } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
const client = getClient()
async function newIssue (): Promise<void> {
showPopup(CreateCustomer, {}, 'top')
}
</script>
<div class="antiNav-subheader">
<Button
icon={IconAdd}
label={lead.string.CreateCustomerLabel}
justify={'left'}
width={'100%'}
kind={'primary'}
gap={'large'}
on:click={newIssue}
/>
</div>
<HeaderButton
{client}
mainActionId={lead.string.CreateCustomerLabel}
visibleActions={[lead.string.CreateCustomerLabel]}
actions={[
{
id: lead.string.CreateCustomerLabel,
label: lead.string.CreateCustomerLabel,
accountRole: AccountRole.User,
callback: newIssue
}
]}
/>
+17 -2
View File
@@ -15,7 +15,17 @@
//
import type { Contact } from '@hcengineering/contact'
import type { Attribute, Class, MarkupBlobRef, Doc, Markup, Ref, Status, Timestamp } from '@hcengineering/core'
import type {
Attribute,
Class,
MarkupBlobRef,
Doc,
Markup,
Ref,
Status,
Timestamp,
Permission
} from '@hcengineering/core'
import { Mixin } from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
@@ -73,7 +83,9 @@ const lead = plugin(leadId, {
},
string: {
Lead: '' as IntlString,
ConfigLabel: '' as IntlString
ConfigLabel: '' as IntlString,
ForbidCreateFunnelPermission: '' as IntlString,
ForbidCreateFunnelPermissionDescription: '' as IntlString
},
attribute: {
State: '' as Ref<Attribute<Status>>
@@ -96,6 +108,9 @@ const lead = plugin(leadId, {
},
space: {
DefaultFunnel: '' as Ref<Funnel>
},
permission: {
ForbidCreateFunnel: '' as Ref<Permission>
}
})
+4 -2
View File
@@ -122,11 +122,13 @@
"HideArchivedVacancies": "Skrýt archivované pozice",
"HideApplicantsFromArchivedVacancies": "Skrýt z archivovaných pozic",
"CreateNewSkills": "Vytvořit nové dovednosti, pokud neexistují",
"SwapFirstAndLastNames": "Vyměňte jméno a příjmení"
"SwapFirstAndLastNames": "Vyměňte jméno a příjmení",
"ForbidCreateVacancyPermission": "Zakázat vytvoření pozice",
"ForbidCreateVacancyPermissionDescription": "Zakazuje uživatelům vytvářet nové volné pozice"
},
"status": {
"ApplicationExists": "Přihláška již existuje",
"TalentRequired": "Vyberte talent",
"VacancyRequired": "Vyberte pozici"
}
}
}
+6 -4
View File
@@ -105,24 +105,26 @@
"MoveApplication": "Zu anderer Stelle verschieben",
"SearchVacancy": "Stelle suchen...",
"Organizations": "Unternehmen",
"TemplateReplace": "Möchten Sie die neue Vorlage anwenden?",
"TemplateReplaceConfirm": "Alle Felder werden mit den Werten der neuen Vorlage überschrieben",
"Apply": "Anwenden",
"OpenVacancyList": "Liste öffnen",
"Export": "Exportieren",
"ConfigLabel": "Rekrutierung",
"ConfigDescription": "Erweiterung zur Verwaltung von Talenten/Bewerbern und Stellen.",
"MyApplications": "Meine Bewerbungen",
"ShowApplications": "Bewerbungen anzeigen",
"GetTalentIds": "Talent-IDs abrufen",
"HideDoneState": "Abgeschlossene Bewerbungen ausblenden",
"HideArchivedVacancies": "Archivierte Stellen ausblenden",
"HideApplicantsFromArchivedVacancies": "Aus archivierten Stellen ausblenden",
"CreateNewSkills": "Neue Fähigkeiten erstellen, wenn keine bestehenden gefunden werden",
"SwapFirstAndLastNames": "Vor- und Nachnamen tauschen"
"SwapFirstAndLastNames": "Vor- und Nachnamen tauschen",
"ForbidCreateVacancyPermission": "Stellenerstellung verbieten",
"ForbidCreateVacancyPermissionDescription": "Verbietet Benutzern das Erstellen neuer Stellenangebote"
},
"status": {
"ApplicationExists": "Bewerbung existiert bereits",
+3 -1
View File
@@ -122,7 +122,9 @@
"HideArchivedVacancies": "Hide archived Vacancies",
"HideApplicantsFromArchivedVacancies": "Hide from archived Vacancies",
"CreateNewSkills": "Create new skills if existing not found",
"SwapFirstAndLastNames": "Swap first and last names"
"SwapFirstAndLastNames": "Swap first and last names",
"ForbidCreateVacancyPermission": "Forbid create vacancy",
"ForbidCreateVacancyPermissionDescription": "Forbid users creating new vacancies"
},
"status": {
"ApplicationExists": "Application already exists",
+3 -1
View File
@@ -119,7 +119,9 @@
"HideArchivedVacancies": "Ocultar vacantes archivadas",
"HideApplicantsFromArchivedVacancies": "Ocultar de vacantes archivadas",
"CreateNewSkills": "Crear nuevas habilidades si no se encuentran las existentes",
"SwapFirstAndLastNames": "Intercambie nombres y apellidos"
"SwapFirstAndLastNames": "Intercambie nombres y apellidos",
"ForbidCreateVacancyPermission": "Prohibir crear vacante",
"ForbidCreateVacancyPermissionDescription": "Prohíbe a los usuarios crear nuevas vacantes"
},
"status": {
"ApplicationExists": "La solicitud ya existe",
+4 -2
View File
@@ -119,11 +119,13 @@
"HideArchivedVacancies": "Masquer les postes vacants archivés",
"HideApplicantsFromArchivedVacancies": "Masquer les candidats des postes vacants archivés",
"CreateNewSkills": "Créer de nouvelles compétences si les existantes ne sont pas trouvées",
"SwapFirstAndLastNames": "Permuter le prénom et le nom"
"SwapFirstAndLastNames": "Permuter le prénom et le nom",
"ForbidCreateVacancyPermission": "Interdire la création de poste vacant",
"ForbidCreateVacancyPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux postes vacants"
},
"status": {
"ApplicationExists": "La candidature existe déjà",
"TalentRequired": "Veuillez sélectionner un talent",
"VacancyRequired": "Veuillez sélectionner un poste vacant"
}
}
}
+3 -1
View File
@@ -120,7 +120,9 @@
"HideArchivedVacancies": "Nascondi posizioni archiviate",
"HideApplicantsFromArchivedVacancies": "Nascondi da posizioni archiviate",
"CreateNewSkills": "Crea nuove competenze se quelle esistenti non vengono trovate",
"SwapFirstAndLastNames": "Scambia nome e cognome"
"SwapFirstAndLastNames": "Scambia nome e cognome",
"ForbidCreateVacancyPermission": "Vieta creazione posizione aperta",
"ForbidCreateVacancyPermissionDescription": "Vieta agli utenti di creare nuove posizioni aperte"
},
"status": {
"ApplicationExists": "La candidatura esiste già",
+3 -1
View File
@@ -119,7 +119,9 @@
"HideArchivedVacancies": "アーカイブされた求人を非表示",
"HideApplicantsFromArchivedVacancies": "アーカイブされた求人から非表示",
"CreateNewSkills": "既存のスキルが見つからない場合は新しいスキルを作成",
"SwapFirstAndLastNames": "名と姓を入れ替える"
"SwapFirstAndLastNames": "名と姓を入れ替える",
"ForbidCreateVacancyPermission": "求人の作成禁止",
"ForbidCreateVacancyPermissionDescription": "ユーザーが新しい求人を作成することを禁止します"
},
"status": {
"ApplicationExists": "応募はすでに存在します",
+3 -1
View File
@@ -119,7 +119,9 @@
"HideArchivedVacancies": "Ocultar vagas arquivadas",
"HideApplicantsFromArchivedVacancies": "Ocultar de vagas arquivadas",
"CreateNewSkills": "Criar novas competências se as existentes não forem encontradas",
"SwapFirstAndLastNames": "Trocar primeiro e último nome"
"SwapFirstAndLastNames": "Trocar primeiro e último nome",
"ForbidCreateVacancyPermission": "Proibir criação de vaga",
"ForbidCreateVacancyPermissionDescription": "Proíbe os utilizadores de criar novas vagas"
},
"status": {
"ApplicationExists": "A candidatura já existe",
+3 -1
View File
@@ -122,7 +122,9 @@
"HideArchivedVacancies": "Скрыть архивные вакансии",
"HideApplicantsFromArchivedVacancies": "Скрыть из архивных вакансии",
"CreateNewSkills": "Создать навыки, если не найдены существующие",
"SwapFirstAndLastNames": "Поменять местами имя и фамилию"
"SwapFirstAndLastNames": "Поменять местами имя и фамилию",
"ForbidCreateVacancyPermission": "Запретить создание вакансии",
"ForbidCreateVacancyPermissionDescription": "Запрещает пользователям создавать новые вакансии"
},
"status": {
"ApplicationExists": "Кандидат уже существует",
+3 -1
View File
@@ -122,7 +122,9 @@
"HideArchivedVacancies": "隐藏已归档的职位",
"HideApplicantsFromArchivedVacancies": "从已归档职位中隐藏",
"CreateNewSkills": "如果未找到现有技能,则创建新技能",
"SwapFirstAndLastNames": "交换名字和姓氏"
"SwapFirstAndLastNames": "交换名字和姓氏",
"ForbidCreateVacancyPermission": "禁止创建职位",
"ForbidCreateVacancyPermissionDescription": "禁止用户创建新职位"
},
"status": {
"ApplicationExists": "申请已存在",
@@ -13,17 +13,24 @@
// limitations under the License.
-->
<script lang="ts">
import { MultipleDraftController } from '@hcengineering/presentation'
import { Button, IconAdd, showPopup } from '@hcengineering/ui'
import { getClient, MultipleDraftController } from '@hcengineering/presentation'
import { HeaderButton, showPopup } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import recruit from '../plugin'
import CreateCandidate from './CreateCandidate.svelte'
import { Analytics } from '@hcengineering/analytics'
import { RecruitEvents } from '@hcengineering/recruit'
import view from '@hcengineering/view'
import { AccountRole } from '@hcengineering/core'
let draftExists = false
const client = getClient()
const draftController = new MultipleDraftController(recruit.mixin.Candidate)
const newRecruitKeyBindingPromise = client
.findOne(view.class.Action, { _id: recruit.action.CreateTalent })
.then((p) => p?.keyBinding)
onDestroy(
draftController.hasNext((res) => {
draftExists = res
@@ -34,35 +41,36 @@
showPopup(CreateCandidate, { shouldSaveDraft: true }, 'top')
Analytics.handleEvent(RecruitEvents.NewTalentButtonClicked)
}
let mainActionId: string | undefined = undefined
let visibleActions: string[] = []
function updateActions (draft: boolean): void {
mainActionId = draft ? recruit.string.ResumeDraft : recruit.string.CreateTalent
visibleActions = [mainActionId]
}
$: updateActions(draftExists)
</script>
<div class="antiNav-subheader">
<Button
icon={IconAdd}
label={draftExists ? recruit.string.ResumeDraft : recruit.string.CreateTalent}
justify={'left'}
kind={'primary'}
width={'100%'}
gap={'large'}
on:click={newCandidate}
>
<div slot="content" class="draft-circle-container">
{#if draftExists}
<div class="draft-circle" />
{/if}
</div>
</Button>
</div>
<style lang="scss">
.draft-circle-container {
margin-left: auto;
}
.draft-circle {
height: 6px;
width: 6px;
background-color: var(--primary-bg-color);
border-radius: 50%;
}
</style>
<HeaderButton
{client}
{mainActionId}
{visibleActions}
actions={[
{
id: recruit.string.CreateTalent,
label: recruit.string.CreateTalent,
accountRole: AccountRole.User,
keyBindingPromise: newRecruitKeyBindingPromise,
callback: newCandidate
},
{
id: recruit.string.ResumeDraft,
label: recruit.string.ResumeDraft,
draft: true,
accountRole: AccountRole.User,
keyBindingPromise: newRecruitKeyBindingPromise,
callback: newCandidate
}
]}
/>
+2 -1
View File
@@ -45,7 +45,8 @@
"@hcengineering/task": "^0.6.20",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/tags": "^0.6.16"
"@hcengineering/tags": "^0.6.16",
"@hcengineering/view": "^0.6.13"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
+11 -2
View File
@@ -13,12 +13,13 @@
// limitations under the License.
//
import type { Attribute, Class, Doc, Mixin, Ref, Status } from '@hcengineering/core'
import type { Attribute, Class, Doc, Mixin, Permission, Ref, Status } from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { ProjectTypeDescriptor, TaskType } from '@hcengineering/task'
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
import type { Applicant, ApplicantMatch, Candidate, Opinion, Review, Vacancy, VacancyList } from './types'
import { Action } from '@hcengineering/view'
export * from './types'
export * from './analytics'
@@ -45,6 +46,9 @@ const recruit = plugin(recruitId, {
descriptors: {
VacancyType: '' as Ref<ProjectTypeDescriptor>
},
action: {
CreateTalent: '' as Ref<Action<Doc, any>>
},
mixin: {
Candidate: '' as Ref<Mixin<Candidate>>,
VacancyList: '' as Ref<Mixin<VacancyList>>,
@@ -63,7 +67,9 @@ const recruit = plugin(recruitId, {
Application: '' as IntlString,
Vacancy: '' as IntlString,
Review: '' as IntlString,
Talent: '' as IntlString
Talent: '' as IntlString,
ForbidCreateVacancyPermission: '' as IntlString,
ForbidCreateVacancyPermissionDescription: '' as IntlString
},
icon: {
RecruitApplication: '' as Asset,
@@ -87,6 +93,9 @@ const recruit = plugin(recruitId, {
},
taskTypes: {
Applicant: '' as Ref<TaskType>
},
permission: {
ForbidCreateVacancy: '' as Ref<Permission>
}
})
@@ -42,6 +42,14 @@
const client = getClient()
let spacePermissions = descriptor.availablePermissions
if (spaceType._id === core.spaceType.SpacesType) {
const additionalPermissionsQuery = createQuery()
additionalPermissionsQuery.query(core.class.Permission, { scope: 'workspace' }, (res) => {
spacePermissions = descriptor.availablePermissions.concat(res.map((r) => r._id))
})
}
let role: Role | undefined
const roleQuery = createQuery()
$: roleQuery.query(core.class.Role, { _id: objectId }, (res) => {
@@ -66,7 +74,7 @@
ObjectBoxPopup,
{
_class: core.class.Permission,
docQuery: { _id: { $in: descriptor.availablePermissions } },
docQuery: { _id: { $in: spacePermissions } },
multiSelect: true,
allowDeselect: true,
selectedObjects: role.permissions
+4 -2
View File
@@ -277,7 +277,9 @@
"IssueStatus": "Stav",
"Extensions": "Rozšíření",
"RoleLabel": "Role: {role}",
"UnsetParentIssue": "Odebrat nadřazený úkol"
"UnsetParentIssue": "Odebrat nadřazený úkol",
"ForbidCreateProjectPermission": "Zakázat vytvoření projektu",
"ForbidCreateProjectPermissionDescription": "Zakazuje uživatelům vytvářet nové projekty"
},
"status": {}
}
}
+3 -1
View File
@@ -287,7 +287,9 @@
"IssueStatus": "Status",
"Extensions": "Erweiterungen",
"RoleLabel": "Rolle: {role}",
"UnsetParentIssue": "Übergeordnete Aufgabe entfernen"
"UnsetParentIssue": "Übergeordnete Aufgabe entfernen",
"ForbidCreateProjectPermission": "Projekterstellung verbieten",
"ForbidCreateProjectPermissionDescription": "Verbietet Benutzern das Erstellen neuer Projekte"
},
"status": {}
}
+3 -1
View File
@@ -287,7 +287,9 @@
"IssueStatus": "Status",
"Extensions": "Extensions",
"RoleLabel": "Role: {role}",
"UnsetParentIssue": "Unset parent issue"
"UnsetParentIssue": "Unset parent issue",
"ForbidCreateProjectPermission": "Forbid create project",
"ForbidCreateProjectPermissionDescription": "Forbid users creating new projects"
},
"status": {}
}
+4 -2
View File
@@ -270,7 +270,9 @@
"IssueStatus": "Estado",
"Extensions": "Extensions",
"RoleLabel": "Role: {role}",
"UnsetParentIssue": "Unset parent issue"
"UnsetParentIssue": "Unset parent issue",
"ForbidCreateProjectPermission": "Prohibir crear proyecto",
"ForbidCreateProjectPermissionDescription": "Prohíbe a los usuarios crear nuevos proyectos"
},
"status": {}
}
}
+4 -2
View File
@@ -270,7 +270,9 @@
"IssueStatus": "Statut",
"Extensions": "Extensions",
"RoleLabel": "Rôle : {role}",
"UnsetParentIssue": "Désélectionner l'issue parent"
"UnsetParentIssue": "Désélectionner l'issue parent",
"ForbidCreateProjectPermission": "Interdire la création de projet",
"ForbidCreateProjectPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux projets"
},
"status": {}
}
}
+3 -1
View File
@@ -270,7 +270,9 @@
"IssueStatus": "Stato",
"Extensions": "Estensioni",
"RoleLabel": "Ruolo: {role}",
"UnsetParentIssue": "Annulla l'issue genitore"
"UnsetParentIssue": "Annulla l'issue genitore",
"ForbidCreateProjectPermission": "Vieta creazione progetto",
"ForbidCreateProjectPermissionDescription": "Vieta agli utenti di creare nuovi progetti"
},
"status": {}
}
+3 -1
View File
@@ -270,7 +270,9 @@
"IssueStatus": "ステータス",
"Extensions": "拡張機能",
"RoleLabel": "役割: {role}",
"UnsetParentIssue": "親イシューの設定を解除"
"UnsetParentIssue": "親イシューの設定を解除",
"ForbidCreateProjectPermission": "プロジェクト作成禁止",
"ForbidCreateProjectPermissionDescription": "ユーザーが新しいプロジェクトを作成することを禁止します"
},
"status": {}
}
+4 -2
View File
@@ -270,7 +270,9 @@
"IssueStatus": "Estado",
"Extensions": "Extensions",
"RoleLabel": "Cargo: {role}",
"UnsetParentIssue": "Desmarcar problema pai"
"UnsetParentIssue": "Desmarcar problema pai",
"ForbidCreateProjectPermission": "Proibir criação de projeto",
"ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos"
},
"status": {}
}
}
+3 -1
View File
@@ -287,7 +287,9 @@
"IssueStatus": "Статус",
"Extensions": "Дополнительно",
"RoleLabel": "Роль: {role}",
"UnsetParentIssue": "Снять родительскую задачу"
"UnsetParentIssue": "Снять родительскую задачу",
"ForbidCreateProjectPermission": "Запретить создание проекта",
"ForbidCreateProjectPermissionDescription": "Запрещает пользователям создавать новые проекты"
},
"status": {}
}
+3 -1
View File
@@ -287,7 +287,9 @@
"IssueStatus": "状态",
"Extensions": "扩展",
"RoleLabel": "角色:{role}",
"UnsetParentIssue": "取消父问题"
"UnsetParentIssue": "取消父问题",
"ForbidCreateProjectPermission": "禁止创建项目",
"ForbidCreateProjectPermissionDescription": "禁止用户创建新项目"
},
"status": {}
}
@@ -14,10 +14,10 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { AccountRole, Ref, Space, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import core, { AccountRole, Ref, Space } from '@hcengineering/core'
import { MultipleDraftController, createQuery, getClient } from '@hcengineering/presentation'
import { TrackerEvents } from '@hcengineering/tracker'
import { Button, ButtonWithDropdown, IconAdd, IconDropdown, SelectPopupValueType, showPopup } from '@hcengineering/ui'
import { HeaderButton, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { onDestroy } from 'svelte'
@@ -27,16 +27,36 @@
export let currentSpace: Ref<Space> | undefined
let closed = true
let draftExists = false
let projectExists = false
let loading = true
const query = createQuery()
const client = getClient()
const draftController = new MultipleDraftController(tracker.ids.IssueDraft)
const newIssueKeyBindingPromise = client
.findOne(view.class.Action, { _id: tracker.action.NewIssue })
.then((p) => p?.keyBinding)
onDestroy(
draftController.hasNext((res) => {
draftExists = res
})
)
async function newIssue (): Promise<void> {
query.query(tracker.class.Project, {}, (res) => {
projectExists = res.length > 0
loading = false
})
function newProject (): void {
closed = false
showPopup(tracker.component.CreateProject, {}, 'top', () => {
closed = true
})
}
function newIssue (): void {
closed = false
Analytics.handleEvent(TrackerEvents.NewIssueButtonClicked)
showPopup(CreateIssue, { space: currentSpace, shouldSaveDraft: true }, 'top', () => {
@@ -44,103 +64,50 @@
})
}
const query = createQuery()
let projectExists = false
query.query(tracker.class.Project, {}, (res) => {
projectExists = res.length > 0
})
$: label = draftExists || !closed ? tracker.string.ResumeDraft : tracker.string.NewIssue
$: dropdownItems = hasAccountRole(getCurrentAccount(), AccountRole.User)
? [
{
id: tracker.string.CreateProject,
label: tracker.string.CreateProject
},
{
id: tracker.string.NewIssue,
label
}
]
: [
{
id: tracker.string.NewIssue,
label
}
]
const client = getClient()
let keys: string[] | undefined = undefined
async function dropdownItemSelected (res?: SelectPopupValueType['id']): Promise<void> {
if (res == null) return
if (res === tracker.string.CreateProject) {
closed = false
showPopup(tracker.component.CreateProject, {}, 'top', () => {
closed = true
})
let mainActionId: string | undefined = undefined
let visibleActions: string[] = []
function updateActions (draft: boolean, project: boolean, closed: boolean): void {
mainActionId = draft || !closed ? tracker.string.ResumeDraft : tracker.string.NewIssue
if (project) {
visibleActions = [tracker.string.CreateProject, mainActionId, tracker.string.Import]
} else {
await newIssue()
visibleActions = [tracker.string.CreateProject]
}
}
void client.findOne(view.class.Action, { _id: tracker.action.NewIssue }).then((p) => (keys = p?.keyBinding))
$: updateActions(draftExists, projectExists, closed)
</script>
<div class="antiNav-subheader">
{#if projectExists}
<ButtonWithDropdown
icon={IconAdd}
justify={'left'}
kind={'primary'}
{label}
on:click={newIssue}
{dropdownItems}
dropdownIcon={IconDropdown}
on:dropdown-selected={(ev) => {
dropdownItemSelected(ev.detail)
}}
mainButtonId={'new-issue'}
showTooltipMain={{
direction: 'bottom',
label,
keys
}}
>
<div slot="content" class="draft-circle-container">
{#if draftExists}
<div class="draft-circle" />
{/if}
</div>
</ButtonWithDropdown>
{:else}
<Button
icon={IconAdd}
justify="left"
kind="primary"
label={tracker.string.CreateProject}
width="100%"
on:click={() => {
showPopup(tracker.component.CreateProject, {}, 'top', () => {
closed = true
})
}}
/>
{/if}
</div>
<style lang="scss">
.draft-circle-container {
margin-left: auto;
padding-right: 12px;
}
.draft-circle {
height: 6px;
width: 6px;
background-color: var(--primary-bg-color);
border-radius: 50%;
}
</style>
<HeaderButton
{loading}
{client}
{mainActionId}
{visibleActions}
actions={[
{
id: tracker.string.CreateProject,
label: tracker.string.CreateProject,
accountRole: AccountRole.User,
callback: newProject
},
{
id: tracker.string.ResumeDraft,
label: tracker.string.ResumeDraft,
draft: true,
keyBindingPromise: newIssueKeyBindingPromise,
callback: newIssue
},
{
id: tracker.string.NewIssue,
label: tracker.string.NewIssue,
keyBindingPromise: newIssueKeyBindingPromise,
callback: newIssue
},
{
id: tracker.string.Import,
label: tracker.string.Import,
accountRole: AccountRole.User,
callback: newIssue
}
]}
/>
+8 -2
View File
@@ -29,7 +29,8 @@ import {
Space,
Status,
Timestamp,
Type
Type,
type Permission
} from '@hcengineering/core'
import { Asset, IntlString, Plugin, Resource, plugin } from '@hcengineering/platform'
import { Preference } from '@hcengineering/preference'
@@ -519,7 +520,9 @@ const pluginState = plugin(trackerId, {
RelatedIssues: '' as IntlString,
Issue: '' as IntlString,
NewProject: '' as IntlString,
UnsetParentIssue: '' as IntlString
UnsetParentIssue: '' as IntlString,
ForbidCreateProjectPermission: '' as IntlString,
ForbidCreateProjectPermissionDescription: '' as IntlString
},
extensions: {
IssueListHeader: '' as ComponentExtensionId,
@@ -529,6 +532,9 @@ const pluginState = plugin(trackerId, {
taskTypes: {
Issue: '' as Ref<TaskType>,
SubIssue: '' as Ref<TaskType>
},
permission: {
ForbidCreateProject: '' as Ref<Permission>
}
})
export default pluginState
+34 -17
View File
@@ -45,7 +45,7 @@ import { BaseMiddleware } from '@hcengineering/server-core'
export class SpacePermissionsMiddleware extends BaseMiddleware implements Middleware {
private whitelistSpaces = new Set<Ref<Space>>()
private assignmentBySpace: Record<Ref<Space>, RolesAssignment> = {}
private permissionsBySpace: Record<Ref<Space>, Record<AccountUuid, Set<Ref<Permission>>>> = {}
private permissionsBySpace: Record<Ref<Space>, Record<AccountUuid, Set<Permission>>> = {}
private typeBySpace: Record<Ref<Space>, Ref<SpaceType>> = {}
wasInit: Promise<void> | boolean = false
@@ -80,11 +80,20 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
}
}
private getPermissions (): Permission[] {
return this.context.modelDb.findAllSync(core.class.Permission, {})
}
private getRoles (spaceTypeId: Ref<SpaceType>): Role[] {
return this.context.modelDb.findAllSync(core.class.Role, { attachedTo: spaceTypeId })
}
private setPermissions (spaceId: Ref<Space>, roles: Role[], assignment: RolesAssignment): void {
private setPermissions (
spaceId: Ref<Space>,
roles: Role[],
assignment: RolesAssignment,
permissions: Permission[]
): void {
for (const role of roles) {
const roleMembers: AccountUuid[] = assignment[role._id] ?? []
@@ -94,7 +103,9 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
}
for (const permission of role.permissions) {
this.permissionsBySpace[spaceId][member].add(permission)
const p = permissions.find((p) => p._id === permission)
if (p === undefined) continue
this.permissionsBySpace[spaceId][member].add(p)
}
}
}
@@ -127,7 +138,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
this.assignmentBySpace[space._id] = requiredValues
this.setPermissions(space._id, this.getRoles(spaceType._id), asMixin)
this.setPermissions(space._id, this.getRoles(spaceType._id), asMixin, this.getPermissions())
}
private isTypedSpaceClass (_class: Ref<Class<Space>>): boolean {
@@ -145,11 +156,16 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
*
* Checks if the required permission is present in the space for the given context
*/
private checkPermission (ctx: MeasureContext<SessionData>, space: Ref<TypedSpace>, id: Ref<Permission>): boolean {
private checkPermission (ctx: MeasureContext<SessionData>, space: Ref<TypedSpace>, tx: TxCUD<Doc>): boolean {
const account = ctx.contextData.account
const permissions = this.permissionsBySpace[space]?.[account.uuid] ?? null
const permissions = this.permissionsBySpace[space]?.[account.uuid] ?? []
for (const permission of permissions) {
if (permission.txClass === undefined || permission.txClass !== tx._class) continue
if (permission.objectClass !== undefined && permission.objectClass !== tx.objectClass) continue
return permission.forbid !== undefined ? !permission.forbid : true
}
return permissions !== null && permissions.has(id)
return true
}
private throwForbidden (): void {
@@ -222,7 +238,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
this.assignmentBySpace[spaceId] = requiredValues
this.permissionsBySpace[tx.objectId] = {}
this.setPermissions(spaceId, this.getRoles(spaceType._id), assignment)
this.setPermissions(spaceId, this.getRoles(spaceType._id), assignment, this.getPermissions())
}
private handleRemove (tx: TxCUD<Space>): void {
@@ -293,7 +309,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
targetRole.permissions = updateTx.operations.permissions
this.permissionsBySpace[spaceId] = {}
this.setPermissions(spaceId, roles, assignment)
this.setPermissions(spaceId, roles, assignment, this.getPermissions())
}
}
@@ -349,20 +365,21 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
const cudTx = tx as TxCUD<Doc>
const h = this.context.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>
this.checkSpacePermissions(ctx, cudTx, cudTx.objectSpace)
if (isSpace) {
this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref<Space>)
}
}
private checkSpacePermissions (ctx: MeasureContext, cudTx: TxCUD<Doc>, targetSpaceId: Ref<Space>): void {
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) {
if (this.checkPermission(ctx, targetSpaceId as Ref<TypedSpace>, core.permission.ForbidDeleteObject)) {
this.throwForbidden()
}
if (!this.checkPermission(ctx, targetSpaceId as Ref<TypedSpace>, cudTx)) {
this.throwForbidden()
}
}
}
@@ -18,7 +18,7 @@ export class DocumentsPage extends CommonPage {
readonly popupMoveDocument: DocumentMovePopup
readonly buttonCreateDocument = (): Locator =>
this.page.locator('div[data-float="navigator"] button[id="new-document"]')
this.page.locator('div[data-float="navigator"] button[id="document-string-CreateDocument"]')
readonly buttonDocumentWrapper = (name: string): Locator =>
this.page.locator(`button.hulyNavItem-container:has-text("${name}")`)
@@ -10,7 +10,8 @@ export class IssuesPage extends CommonTrackerPage {
modelSelectorAll = (): Locator => this.page.locator('label[data-id="tab-all"]')
issues = (): Locator => this.page.locator('.antiPanel-navigator').locator('text="Issues"')
subIssues = (): Locator => this.page.locator('button:has-text("Add sub-issue")')
newIssue = (): Locator => this.page.locator('#new-issue')
newIssue = (): Locator => this.page.locator('#tracker-string-NewIssue')
draftIssue = (): Locator => this.page.locator('#tracker-string-ResumeDraft')
modelSelectorActive = (): Locator => this.page.locator('label[data-id="tab-active"]')
modelSelectorBacklog = (): Locator => this.page.locator('label[data-id="tab-backlog"]')
buttonCreateNewIssue = (): Locator => this.page.locator('button > div', { hasText: 'New issue' })
@@ -156,7 +157,7 @@ export class IssuesPage extends CommonTrackerPage {
estimationSpan = (): Locator => this.page.locator('.estimation-container >> span').first()
okButton = (): Locator => this.page.getByRole('button', { name: 'Ok', exact: true })
newIssueButton = (): Locator => this.page.locator('#new-issue')
newIssueButton = (): Locator => this.page.locator('#tracker-string-NewIssue')
issueNameInput = (): Locator => this.page.locator('#issue-name >> input')
issueDescriptionInput = (): Locator => this.page.locator('#issue-description >> [contenteditable]')
statusEditor = (): Locator => this.page.locator('#status-editor')
@@ -232,6 +233,10 @@ export class IssuesPage extends CommonTrackerPage {
await this.newIssue().click()
}
async clickOnDraftIssue (): Promise<void> {
await this.draftIssue().click()
}
async navigateToMyIssues (): Promise<void> {
await this.myIssuesButton().click()
}
+1 -1
View File
@@ -45,7 +45,7 @@ test.describe('Tracker sub-issues tests', () => {
await fillIssueForm(page, props)
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await issuesPage.clickOnNewIssue()
await issuesPage.clickOnDraftIssue()
await checkIssueDraft(page, props)
})
+1 -1
View File
@@ -161,7 +161,7 @@ test.describe('Tracker tests', () => {
await issuesPage.inputTextPlaceholderFill('1')
await issuesPage.setDueDate('19')
await issuesPage.pressEscapeTwice()
await issuesPage.clickOnNewIssue()
await issuesPage.clickOnDraftIssue()
await checkIssueDraft(page, {
name: issueName,
description: issueName,