mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
[EZQMS-732, EZQMS-737]: Restrict type configuration, allow deleting roles (#5512)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
bb29cebdd9
commit
3cbec84e5b
@ -16,6 +16,7 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import {
|
||||
Account,
|
||||
AccountRole,
|
||||
AnyAttribute,
|
||||
AttachedData,
|
||||
AttachedDoc,
|
||||
@ -784,3 +785,9 @@ export function reduceCalls<T extends (...args: ReduceParameters<T>) => Promise<
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isOwnerOrMaintainer (): boolean {
|
||||
const account = getCurrentAccount()
|
||||
|
||||
return [AccountRole.Owner, AccountRole.Maintainer].includes(account.role)
|
||||
}
|
||||
|
@ -53,6 +53,7 @@
|
||||
export let enableInlineCommands: boolean = true
|
||||
export let isScrollable: boolean = true
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
export let readonly: boolean = false
|
||||
|
||||
export let attachFile: FileAttachFunction | undefined = undefined
|
||||
|
||||
@ -99,6 +100,8 @@
|
||||
$: if (!modified && rawValue !== content) modified = true
|
||||
$: dispatch('change', modified)
|
||||
|
||||
$: textEditor?.setEditable(!readonly)
|
||||
|
||||
export function submit (): void {
|
||||
textEditor.submit()
|
||||
}
|
||||
@ -292,6 +295,7 @@
|
||||
id="imageInput"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
disabled={readonly}
|
||||
on:change={fileSelected}
|
||||
/>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@ -302,7 +306,7 @@
|
||||
class:antiIndented={kind === 'indented'}
|
||||
class:focusable={(mode === Mode.Edit || alwaysEdit) && focused}
|
||||
on:click={() => {
|
||||
if (alwaysEdit && focused) {
|
||||
if (alwaysEdit && focused && !readonly) {
|
||||
textEditor?.focus()
|
||||
}
|
||||
}}
|
||||
@ -313,7 +317,7 @@
|
||||
{#if label}
|
||||
<div class="label"><Label {label} /></div>
|
||||
{/if}
|
||||
{#if mode !== Mode.View || alwaysEdit}
|
||||
{#if (mode !== Mode.View || alwaysEdit) && !readonly}
|
||||
<StyledTextEditor
|
||||
{placeholder}
|
||||
{showButtons}
|
||||
@ -362,7 +366,7 @@
|
||||
</ShowMore>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !alwaysEdit && !hideExtraButtons}
|
||||
{#if !alwaysEdit && !hideExtraButtons && !readonly}
|
||||
<div class="flex flex-reverse">
|
||||
<ActionIcon
|
||||
size={'medium'}
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
export let embedded = false
|
||||
export let selected: string | undefined
|
||||
export let disabled: boolean = false
|
||||
|
||||
interface Category {
|
||||
id: string
|
||||
@ -181,7 +182,16 @@
|
||||
{#if emoji !== undefined}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="element" class:selected={emoji === selected} on:click={() => dispatch('close', emoji)}>
|
||||
<div
|
||||
class="element"
|
||||
class:selected={emoji === selected}
|
||||
class:disabled
|
||||
on:click={() => {
|
||||
if (disabled) return
|
||||
|
||||
dispatch('close', emoji)
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
{/if}
|
||||
@ -248,13 +258,19 @@
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-popup-hover);
|
||||
&:not(&.disabled) {
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-popup-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--theme-popup-header);
|
||||
border: 1px solid var(--theme-popup-divider);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -22,6 +22,7 @@
|
||||
import recruit from '../plugin'
|
||||
|
||||
export let type: ProjectType
|
||||
export let disabled: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@ -29,6 +30,9 @@
|
||||
const customKeys = getFiltredKeys(hierarchy, type._class, []).filter((key) => key.attr.isCustom)
|
||||
|
||||
async function onDescriptionChange (value: string) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
await client.diffUpdate(type, { description: value })
|
||||
}
|
||||
</script>
|
||||
@ -45,6 +49,8 @@
|
||||
maxHeight={'card'}
|
||||
showButtons={false}
|
||||
content={type.description ?? ''}
|
||||
focusable={!disabled}
|
||||
readonly={disabled}
|
||||
on:value={(evt) => onDescriptionChange(evt.detail)}
|
||||
/>
|
||||
{/key}
|
||||
@ -58,18 +64,21 @@
|
||||
<span class="antiSection-header__title">
|
||||
<Label label={tracker.string.RelatedIssues} />
|
||||
</span>
|
||||
<div class="buttons-group small-gap">
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={IconAdd}
|
||||
label={undefined}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
on:click={() => showPopup(tracker.component.CreateIssueTemplate, { relatedTo: type })}
|
||||
/>
|
||||
</div>
|
||||
{#if !disabled}
|
||||
<div class="buttons-group small-gap">
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={IconAdd}
|
||||
label={undefined}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
{disabled}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
on:click={() => showPopup(tracker.component.CreateIssueTemplate, { relatedTo: type })}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<Component is={tracker.component.RelatedIssueTemplates} props={{ object: type }} />
|
||||
@ -77,6 +86,6 @@
|
||||
</div>
|
||||
{#if customKeys && customKeys.length > 0}
|
||||
<div class="antiSection mb-9">
|
||||
<AttributesBar object={type} _class={type._class} keys={customKeys} />
|
||||
<AttributesBar object={type} _class={type._class} keys={customKeys} readonly={disabled} />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -111,6 +111,8 @@
|
||||
"Roles": "Roles",
|
||||
"RoleName": "Role name",
|
||||
"Permissions": "Permissions",
|
||||
"Assignees": "Assignees"
|
||||
"Assignees": "Assignees",
|
||||
"DeleteRole": "Delete role",
|
||||
"DeleteRoleConfirmation": "Are you sure you want to delete this role? All users with this role will lose their permissions."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +102,8 @@
|
||||
"Collections": "Colecciones",
|
||||
"ClassColon": "Clase:",
|
||||
"Branding": "Marca",
|
||||
"Assignees": "Atribuídos"
|
||||
"Assignees": "Atribuídos",
|
||||
"DeleteRole": "Eliminar función",
|
||||
"DeleteRoleConfirmation": "¿Está seguro de que desea eliminar esta función? Todos los usuarios con este rol perderán sus permisos"
|
||||
}
|
||||
}
|
@ -102,6 +102,8 @@
|
||||
"Collections": "Coleções",
|
||||
"ClassColon": "Classe:",
|
||||
"Branding": "Marca",
|
||||
"Assignees": ""
|
||||
"Assignees": "Atribuídos",
|
||||
"DeleteRole": "Eliminar função",
|
||||
"DeleteRoleConfirmation": "Tem a certeza de que pretende eliminar esta função? Todos os utilizadores com esta função perderão as suas permissões."
|
||||
}
|
||||
}
|
@ -112,6 +112,8 @@
|
||||
"Roles": "Роли",
|
||||
"RoleName": "Название роли",
|
||||
"Permissions": "Разрешения",
|
||||
"Assignees": "Назначенные"
|
||||
"Assignees": "Назначенные",
|
||||
"DeleteRole": "Удалить роль",
|
||||
"DeleteRoleConfirmation": "Вы действительно хотите удалить эту роль? Все пользователи с этой ролью потеряют имеющиеся разрешения."
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
export let ofClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let showHierarchy: boolean = false
|
||||
export let showTitle: boolean = !showHierarchy
|
||||
|
||||
export let disabled: boolean = true
|
||||
export let attributeMapper:
|
||||
| {
|
||||
component: AnySvelteComponent
|
||||
@ -73,12 +73,20 @@
|
||||
}
|
||||
|
||||
export function createAttribute (ev: MouseEvent): void {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(TypesPopup, { _class }, getEventPositionElement(ev), (_id) => {
|
||||
if (_id !== undefined) $settingsStore = { component: CreateAttribute, props: { selectedType: _id, _class } }
|
||||
})
|
||||
}
|
||||
|
||||
function editLabel (evt: MouseEvent): void {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(EditClassLabel, { clazz }, getEventPositionElement(evt))
|
||||
}
|
||||
|
||||
@ -113,7 +121,7 @@
|
||||
const handleSelect = async (event: CustomEvent): Promise<void> => {
|
||||
selected = event.detail as AnyAttribute
|
||||
const exist = (await client.findOne(selected.attributeOf, { [selected.name]: { $exists: true } })) !== undefined
|
||||
$settingsStore = { id: selected._id, component: EditAttribute, props: { attribute: selected, exist } }
|
||||
$settingsStore = { id: selected._id, component: EditAttribute, props: { attribute: selected, exist, disabled } }
|
||||
}
|
||||
onDestroy(() => {
|
||||
if (selected !== undefined) clearSettingsStore()
|
||||
@ -128,7 +136,7 @@
|
||||
</div>
|
||||
{#if clazz.kind === ClassifierKind.MIXIN && hierarchy.hasMixin(clazz, settings.mixin.UserMixin)}
|
||||
<div class="ml-2">
|
||||
<ActionIcon icon={IconEdit} size="small" action={editLabel} />
|
||||
<ActionIcon icon={IconEdit} size="small" action={editLabel} {disabled} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@ -137,7 +145,7 @@
|
||||
<div class="hulyTableAttr-container">
|
||||
<div class="hulyTableAttr-header font-medium-12" class:withButton={showHierarchy}>
|
||||
{#if showHierarchy}
|
||||
<ModernButton icon={IconSettings} kind={'secondary'} size={'small'} hasMenu>
|
||||
<ModernButton icon={IconSettings} kind={'secondary'} size={'small'} {disabled} hasMenu>
|
||||
<Label label={settings.string.ClassColon} />
|
||||
<ObjectPresenter _class={clazzHierarchy._class} objectId={clazzHierarchy._id} value={clazzHierarchy} />
|
||||
</ModernButton>
|
||||
@ -149,6 +157,7 @@
|
||||
kind={'primary'}
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
{disabled}
|
||||
on:click={(ev) => {
|
||||
createAttribute(ev)
|
||||
}}
|
||||
|
@ -35,6 +35,8 @@
|
||||
|
||||
export let attribute: AnyAttribute
|
||||
export let exist: boolean
|
||||
export let disabled: boolean = true
|
||||
|
||||
let name: string
|
||||
let type: Type<PropertyType> | undefined = attribute.type
|
||||
let index: IndexKind | undefined = attribute.index
|
||||
@ -47,6 +49,10 @@
|
||||
translate(attribute.label, {}, $themeStore.language).then((p) => (name = p))
|
||||
|
||||
async function save (): Promise<void> {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const update: DocumentUpdate<AnyAttribute> = {}
|
||||
const newLabel = getEmbeddedLabel(name)
|
||||
if (newLabel !== attribute.label) {
|
||||
@ -98,6 +104,7 @@
|
||||
selectType(e.detail)
|
||||
}
|
||||
const handleChange = (e: any) => {
|
||||
if (disabled) return
|
||||
type = e.detail?.type
|
||||
index = e.detail?.index
|
||||
defaultValue = e.detail?.defaultValue
|
||||
@ -109,14 +116,16 @@
|
||||
type={'type-aside'}
|
||||
okLabel={presentation.string.Save}
|
||||
okAction={save}
|
||||
canSave={!(name === undefined || name.trim().length === 0)}
|
||||
canSave={!(name === undefined || name.trim().length === 0) && !disabled}
|
||||
onCancel={() => {
|
||||
clearSettingsStore()
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<ButtonIcon icon={IconDelete} size={'small'} kind={'tertiary'} />
|
||||
<ButtonIcon icon={IconCopy} size={'small'} kind={'tertiary'} />
|
||||
{#if !disabled}
|
||||
<ButtonIcon icon={IconDelete} size={'small'} kind={'tertiary'} {disabled} />
|
||||
<ButtonIcon icon={IconCopy} size={'small'} kind={'tertiary'} {disabled} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="hulyModal-content__titleGroup">
|
||||
{#if attribute.isCustom}
|
||||
@ -124,7 +133,7 @@
|
||||
<Label label={setting.string.Custom} />
|
||||
</div>
|
||||
{/if}
|
||||
<ModernEditbox bind:value={name} label={core.string.Name} size={'large'} kind={'ghost'} />
|
||||
<ModernEditbox bind:value={name} label={core.string.Name} size={'large'} kind={'ghost'} {disabled} />
|
||||
</div>
|
||||
<div class="hulyModal-content__settingsSet">
|
||||
<div class="hulyModal-content__settingsSet-line">
|
||||
@ -141,6 +150,7 @@
|
||||
width="8rem"
|
||||
bind:selected={selectedType}
|
||||
on:selected={handleSelect}
|
||||
{disabled}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@ -150,10 +160,11 @@
|
||||
props={{
|
||||
type,
|
||||
defaultValue,
|
||||
editable: !exist,
|
||||
editable: !exist && !disabled,
|
||||
kind: 'regular',
|
||||
size: 'large'
|
||||
}}
|
||||
{disabled}
|
||||
on:change={handleChange}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -15,7 +15,16 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import core, { Class, Doc, IdMap, Ref, SpaceType, WithLookup, toIdMap } from '@hcengineering/core'
|
||||
import core, {
|
||||
Class,
|
||||
Doc,
|
||||
IdMap,
|
||||
Ref,
|
||||
SpaceType,
|
||||
WithLookup,
|
||||
isOwnerOrMaintainer,
|
||||
toIdMap
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
Location,
|
||||
resolvedLocationStore,
|
||||
@ -43,6 +52,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const canEdit = isOwnerOrMaintainer()
|
||||
|
||||
let visibleSecondNav: boolean = true
|
||||
let type: WithLookup<SpaceType> | undefined
|
||||
@ -134,32 +144,34 @@
|
||||
>
|
||||
{#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 })
|
||||
}}
|
||||
/>
|
||||
{#if canEdit}
|
||||
<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 })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<Breadcrumbs
|
||||
items={bcItems}
|
||||
size="large"
|
||||
@ -170,13 +182,21 @@
|
||||
{#if editorDescriptor !== undefined}
|
||||
{#if subEditor === undefined}
|
||||
{#key type._id}
|
||||
<SpaceTypeEditorComponent {type} {descriptor} {editorDescriptor} {visibleSecondNav} on:change />
|
||||
<SpaceTypeEditorComponent
|
||||
{type}
|
||||
{descriptor}
|
||||
{editorDescriptor}
|
||||
{visibleSecondNav}
|
||||
readonly={!canEdit}
|
||||
on:change
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={subEditor}
|
||||
bind:name={subItemName}
|
||||
bind:icon={subItemIcon}
|
||||
readonly={!canEdit}
|
||||
spaceType={type}
|
||||
{descriptor}
|
||||
objectId={selectedSubObjectId}
|
||||
|
@ -16,10 +16,13 @@
|
||||
<script lang="ts">
|
||||
import { Button, IconAdd, showPopup } from '@hcengineering/ui'
|
||||
import CreateSpaceType from './CreateSpaceType.svelte'
|
||||
import { isOwnerOrMaintainer } from '@hcengineering/core'
|
||||
|
||||
function handleAdd (): void {
|
||||
showPopup(CreateSpaceType, {}, 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button id="new-space-type" icon={IconAdd} kind="link" size="small" on:click={handleAdd} />
|
||||
{#if isOwnerOrMaintainer()}
|
||||
<Button id="new-space-type" icon={IconAdd} kind="link" size="small" on:click={handleAdd} />
|
||||
{/if}
|
||||
|
@ -13,18 +13,32 @@
|
||||
// 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 { AttributeEditor, MessageBox, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import core, { Permission, Ref, Role, SpaceType, SpaceTypeDescriptor } from '@hcengineering/core'
|
||||
import {
|
||||
ButtonIcon,
|
||||
Icon,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
IconSettings,
|
||||
Label,
|
||||
Scroller,
|
||||
getCurrentResolvedLocation,
|
||||
navigate,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { ObjectBoxPopup } from '@hcengineering/view-resources'
|
||||
import { deleteSpaceTypeRole } from '@hcengineering/setting'
|
||||
|
||||
import PersonIcon from '../icons/Person.svelte'
|
||||
import settingRes from '../../plugin'
|
||||
import { clearSettingsStore } from '../../store'
|
||||
|
||||
export let spaceType: SpaceType
|
||||
export let descriptor: SpaceTypeDescriptor
|
||||
export let objectId: Ref<Role>
|
||||
export let name: string | undefined
|
||||
export let readonly: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@ -44,7 +58,7 @@
|
||||
}
|
||||
|
||||
function handleEditPermissions (evt: Event): void {
|
||||
if (role === undefined || descriptor === undefined) {
|
||||
if (role === undefined || descriptor === undefined || readonly) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -78,6 +92,36 @@
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function handleDeleteRole (): Promise<void> {
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: settingRes.string.DeleteRole,
|
||||
message: settingRes.string.DeleteRoleConfirmation
|
||||
},
|
||||
'top',
|
||||
(result?: boolean) => {
|
||||
if (result === true) {
|
||||
void performDeleteRole()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function performDeleteRole (): Promise<void> {
|
||||
if (role === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await deleteSpaceTypeRole(client, role, spaceType.targetClass)
|
||||
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.path.length = 5
|
||||
|
||||
clearSettingsStore()
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if role !== undefined}
|
||||
@ -86,16 +130,43 @@
|
||||
<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 class="hulyComponent-content__header mb-6 gap-2">
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
size="large"
|
||||
kind="secondary"
|
||||
disabled={readonly}
|
||||
on:click={handleDeleteRole}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={PersonIcon}
|
||||
size="large"
|
||||
iconProps={{ size: 'small' }}
|
||||
kind="secondary"
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div class="name" class:editable={!readonly}>
|
||||
<AttributeEditor
|
||||
_class={core.class.Role}
|
||||
object={role}
|
||||
key="name"
|
||||
editKind="modern-ghost-large"
|
||||
editable={!readonly}
|
||||
/>
|
||||
</div>
|
||||
</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} />
|
||||
<ButtonIcon
|
||||
kind="primary"
|
||||
icon={IconEdit}
|
||||
size="small"
|
||||
on:click={handleEditPermissions}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if permissions.length > 0}
|
||||
@ -128,3 +199,18 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.name {
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
|
||||
&.editable {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -32,6 +32,7 @@
|
||||
export let descriptor: SpaceTypeDescriptor | undefined
|
||||
export let editorDescriptor: SpaceTypeEditor
|
||||
export let visibleSecondNav: boolean = true
|
||||
export let readonly: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@ -88,6 +89,7 @@
|
||||
<div bind:this={sectionRefs[section.id]} class:hulyTableAttr-container={!section.withoutContainer}>
|
||||
<Component
|
||||
is={section.component}
|
||||
disabled={readonly}
|
||||
props={{
|
||||
type,
|
||||
descriptor
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
export let type: SpaceType | undefined
|
||||
export let descriptor: SpaceTypeDescriptor | undefined
|
||||
export let disabled: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
let shortDescription = type?.shortDescription ?? ''
|
||||
@ -43,7 +44,7 @@
|
||||
}
|
||||
|
||||
async function attributeUpdated<T extends keyof SpaceType> (field: T, value: SpaceType[T]): Promise<void> {
|
||||
if (type === undefined || type[field] === value) {
|
||||
if (disabled || type === undefined || type[field] === value) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -61,6 +62,7 @@
|
||||
size="large"
|
||||
label={settingRes.string.SpaceTypeTitle}
|
||||
value={type?.name ?? ''}
|
||||
{disabled}
|
||||
on:blur={(evt) => {
|
||||
attributeUpdated('name', evt.detail)
|
||||
}}
|
||||
@ -82,6 +84,7 @@
|
||||
height="4.5rem"
|
||||
margin="var(--spacing-2) 0"
|
||||
noFocusBorder
|
||||
{disabled}
|
||||
bind:value={shortDescription}
|
||||
on:change={() => {
|
||||
attributeUpdated('shortDescription', shortDescription)
|
||||
|
@ -19,8 +19,9 @@
|
||||
|
||||
export let type: SpaceType | undefined
|
||||
export let descriptor: SpaceTypeDescriptor | undefined
|
||||
export let disabled: boolean = true
|
||||
</script>
|
||||
|
||||
{#if type !== undefined && descriptor !== undefined}
|
||||
<ClassAttributes ofClass={descriptor.baseClass} _class={type.targetClass} showHierarchy />
|
||||
<ClassAttributes ofClass={descriptor.baseClass} _class={type.targetClass} {disabled} showHierarchy />
|
||||
{/if}
|
||||
|
@ -25,6 +25,7 @@
|
||||
|
||||
export let type: SpaceType | undefined
|
||||
export let descriptor: SpaceTypeDescriptor | undefined
|
||||
export let disabled: boolean = true
|
||||
|
||||
let roles: Role[] = []
|
||||
const rolesQuery = createQuery()
|
||||
@ -62,6 +63,7 @@
|
||||
kind="primary"
|
||||
icon={IconAdd}
|
||||
size="small"
|
||||
{disabled}
|
||||
on:click={(ev) => {
|
||||
$settingsStore = { id: 'createRole', component: CreateRole, props: { type, descriptor } }
|
||||
}}
|
||||
|
@ -99,6 +99,8 @@ export default mergeIds(settingId, setting, {
|
||||
CountSpaces: '' as IntlString,
|
||||
RoleName: '' as IntlString,
|
||||
Permissions: '' as IntlString,
|
||||
Assignees: '' as IntlString
|
||||
Assignees: '' as IntlString,
|
||||
DeleteRole: '' as IntlString,
|
||||
DeleteRoleConfirmation: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -130,3 +130,34 @@ export async function createSpaceTypeRoles (
|
||||
await createSpaceTypeRole(tx, spaceType, { name, permissions }, _id)
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSpaceTypeRole (
|
||||
client: TxOperations,
|
||||
role: Role,
|
||||
targetClass: Ref<Class<Space>>
|
||||
): Promise<void> {
|
||||
const attribute = await client.findOne(core.class.Attribute, { name: role._id, attributeOf: targetClass })
|
||||
const ops = client.apply(role._id)
|
||||
|
||||
await ops.removeCollection(
|
||||
core.class.Role,
|
||||
core.space.Model,
|
||||
role._id,
|
||||
role.attachedTo,
|
||||
role.attachedToClass,
|
||||
'roles'
|
||||
)
|
||||
if (attribute !== undefined) {
|
||||
const mixins = await client.findAll(targetClass, {})
|
||||
for (const mixin of mixins) {
|
||||
await ops.updateMixin(mixin._id, mixin._class, mixin.space, targetClass, {
|
||||
[attribute.name]: undefined
|
||||
})
|
||||
}
|
||||
|
||||
await ops.remove(attribute)
|
||||
}
|
||||
|
||||
// remove all the assignments
|
||||
await ops.commit()
|
||||
}
|
||||
|
@ -19,8 +19,9 @@
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
export let disabled: boolean = true
|
||||
</script>
|
||||
|
||||
{#if descriptor !== undefined && type !== undefined}
|
||||
<ComponentExtensions extension={task.extensions.ProjectEditorExtension} props={{ type }} />
|
||||
<ComponentExtensions extension={task.extensions.ProjectEditorExtension} props={{ type, disabled }} />
|
||||
{/if}
|
||||
|
@ -19,12 +19,13 @@
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
export let disabled: boolean = true
|
||||
</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={() => {}} />
|
||||
<ButtonIcon kind="primary" icon={IconAdd} size="small" {disabled} on:click={() => {}} />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -19,12 +19,13 @@
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
export let disabled: boolean = true
|
||||
</script>
|
||||
|
||||
<SpaceTypeGeneralSectionEditor {type} {descriptor}>
|
||||
<SpaceTypeGeneralSectionEditor {type} {descriptor} {disabled}>
|
||||
<svelte:fragment slot="extra">
|
||||
{#if descriptor?.editor}
|
||||
<Component is={descriptor.editor} props={{ type }} />
|
||||
<Component is={descriptor.editor} props={{ type }} {disabled} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SpaceTypeGeneralSectionEditor>
|
||||
|
@ -27,6 +27,7 @@
|
||||
|
||||
export let type: ProjectType | undefined
|
||||
export let descriptor: ProjectTypeDescriptor | undefined
|
||||
export let disabled: boolean = true
|
||||
|
||||
let taskTypes: TaskType[] = []
|
||||
const taskTypesQuery = createQuery()
|
||||
@ -62,7 +63,11 @@
|
||||
kind="primary"
|
||||
icon={IconAdd}
|
||||
size="small"
|
||||
{disabled}
|
||||
on:click={(ev) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
$settingsStore = { id: 'createTaskType', component: CreateTaskType, props: { type, descriptor } }
|
||||
}}
|
||||
/>
|
||||
|
@ -57,6 +57,7 @@
|
||||
export let icon: Asset | undefined
|
||||
export let canDelete: boolean = true
|
||||
export let selectableStates: Status[] = []
|
||||
export let readonly: boolean = true
|
||||
|
||||
$: _taskType = $taskTypeStore.get(taskType._id) as TaskType
|
||||
$: _type = $typeStore.get(type._id) as ProjectType
|
||||
@ -96,6 +97,7 @@
|
||||
!selectableStates.some((it) => it.name === value)
|
||||
|
||||
async function save (): Promise<void> {
|
||||
if (readonly) return
|
||||
if (total > 0 && value.trim() !== status?.name?.trim()) {
|
||||
// We should ask for changes approve.
|
||||
showPopup(
|
||||
@ -226,6 +228,7 @@
|
||||
oldStatus: Ref<Status>,
|
||||
newStatus: Ref<Status>
|
||||
): Promise<void> {
|
||||
if (readonly) return
|
||||
const projects = await client.findAll(task.class.Project, { type: type._id })
|
||||
while (true) {
|
||||
const docs = await client.findAll(
|
||||
@ -250,7 +253,7 @@
|
||||
}
|
||||
|
||||
function onDelete (): void {
|
||||
if (status === undefined) return
|
||||
if (status === undefined || readonly) return
|
||||
const estatus = status
|
||||
showPopup(
|
||||
DeleteStateConfirmationPopup,
|
||||
@ -293,6 +296,7 @@
|
||||
}
|
||||
|
||||
function onDuplicate (): void {
|
||||
if (readonly) return
|
||||
let pattern = ''
|
||||
let inc = 2
|
||||
|
||||
@ -336,35 +340,38 @@
|
||||
type={'type-aside'}
|
||||
okLabel={status === undefined ? presentation.string.Create : presentation.string.Save}
|
||||
okAction={save}
|
||||
canSave={needUpdate}
|
||||
canSave={needUpdate && !readonly}
|
||||
onCancel={() => {
|
||||
clearSettingsStore()
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
size={'small'}
|
||||
kind={'tertiary'}
|
||||
disabled={status === undefined || !canDelete}
|
||||
on:click={onDelete}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={IconCopy}
|
||||
size={'small'}
|
||||
kind={'tertiary'}
|
||||
disabled={status === undefined}
|
||||
on:click={onDuplicate}
|
||||
/>
|
||||
{#if !readonly}
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
size={'small'}
|
||||
kind={'tertiary'}
|
||||
disabled={status === undefined || !canDelete || readonly}
|
||||
on:click={onDelete}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={IconCopy}
|
||||
size={'small'}
|
||||
kind={'tertiary'}
|
||||
disabled={status === undefined || readonly}
|
||||
on:click={onDuplicate}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="hulyModal-content__titleGroup">
|
||||
<ModernEditbox bind:value label={task.string.StatusName} size={'large'} kind={'ghost'} />
|
||||
<ModernEditbox bind:value label={task.string.StatusName} size={'large'} kind={'ghost'} disabled={readonly} />
|
||||
<TextArea
|
||||
placeholder={task.string.Description}
|
||||
width={'100%'}
|
||||
height={'4.5rem'}
|
||||
margin={'var(--spacing-1) var(--spacing-2)'}
|
||||
noFocusBorder
|
||||
disabled={readonly}
|
||||
bind:value={description}
|
||||
/>
|
||||
</div>
|
||||
@ -374,7 +381,7 @@
|
||||
<ButtonMenu
|
||||
items={categories}
|
||||
selected={category}
|
||||
disabled={!allowEditCategory}
|
||||
disabled={!allowEditCategory || readonly}
|
||||
icon={categories.find((it) => it.id === category)?.icon}
|
||||
label={categories.find((it) => it.id === category)?.label}
|
||||
kind={'secondary'}
|
||||
@ -395,6 +402,7 @@
|
||||
label={items[selected].label}
|
||||
kind={'secondary'}
|
||||
size={'small'}
|
||||
disabled={readonly}
|
||||
on:selected={(event) => {
|
||||
if (event.detail) {
|
||||
selected = items.findIndex((it) => it.id === event.detail)
|
||||
@ -413,8 +421,10 @@
|
||||
<ColorsPopup
|
||||
selected={getPlatformColorDef(color ?? 0, $themeStore.dark).name}
|
||||
embedded
|
||||
disabled={readonly}
|
||||
columns={'auto'}
|
||||
on:close={(evt) => {
|
||||
if (readonly) return
|
||||
color = evt.detail
|
||||
icon = undefined
|
||||
}}
|
||||
@ -423,7 +433,9 @@
|
||||
<EmojiPopup
|
||||
embedded
|
||||
selected={fromCodePoint(color ?? 0)}
|
||||
disabled={readonly}
|
||||
on:close={(evt) => {
|
||||
if (readonly) return
|
||||
color = evt.detail.codePointAt(0)
|
||||
icon = iconWithEmoji
|
||||
}}
|
||||
|
@ -26,6 +26,7 @@
|
||||
export let taskType: TaskType
|
||||
export let type: ProjectType
|
||||
export let states: Status[] = []
|
||||
export let readonly: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -35,6 +36,7 @@
|
||||
let opened: Ref<Status> | undefined
|
||||
|
||||
function dragswap (ev: MouseEvent, i: number): boolean {
|
||||
if (readonly) return false
|
||||
const s = selected as number
|
||||
if (i < s) {
|
||||
return ev.offsetY < elements[i].offsetHeight / 2
|
||||
@ -45,6 +47,7 @@
|
||||
}
|
||||
|
||||
function dragover (ev: MouseEvent, i: number): void {
|
||||
if (readonly) return
|
||||
const s = selected as number
|
||||
if (dragswap(ev, i)) {
|
||||
;[states[i], states[s]] = [states[s], states[i]]
|
||||
@ -53,6 +56,7 @@
|
||||
}
|
||||
|
||||
function onMove (to: number): void {
|
||||
if (readonly) return
|
||||
dispatch('move', {
|
||||
stateID: dragState,
|
||||
position: to
|
||||
@ -130,7 +134,8 @@
|
||||
color,
|
||||
icons,
|
||||
canDelete: sameCategory.length > 1,
|
||||
selectableStates: sameCategory.filter((it) => it._id !== _status._id)
|
||||
selectableStates: sameCategory.filter((it) => it._id !== _status._id),
|
||||
readonly
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -151,7 +156,7 @@
|
||||
bind:this={elements[prevIndex + i]}
|
||||
class="hulyTableAttr-content__row"
|
||||
class:selected={state._id === opened}
|
||||
draggable={true}
|
||||
draggable={!readonly}
|
||||
on:click={() => {
|
||||
handleSelect(state)
|
||||
}}
|
||||
|
@ -41,6 +41,7 @@
|
||||
export let objectId: Ref<TaskType>
|
||||
export let name: string | undefined
|
||||
export let icon: Asset | undefined
|
||||
export let readonly: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@ -79,6 +80,9 @@
|
||||
}
|
||||
|
||||
function selectIcon (el: MouseEvent): void {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
const icons: Asset[] = [descriptor[0].icon]
|
||||
|
||||
showPopup(
|
||||
@ -94,7 +98,7 @@
|
||||
}
|
||||
|
||||
function handleAddStatus (): void {
|
||||
if (taskType === undefined) {
|
||||
if (taskType === undefined || readonly) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -134,14 +138,18 @@
|
||||
}
|
||||
void client.diffUpdate(taskType, { kind: evt.detail })
|
||||
}}
|
||||
{readonly}
|
||||
/>
|
||||
<ButtonIcon
|
||||
icon={TaskTypeIcon}
|
||||
iconProps={{ value: taskType }}
|
||||
size={'large'}
|
||||
kind={'secondary'}
|
||||
on:click={selectIcon}
|
||||
/>
|
||||
{#if !readonly}
|
||||
<ButtonIcon
|
||||
icon={TaskTypeIcon}
|
||||
iconProps={{ value: taskType }}
|
||||
size={'large'}
|
||||
kind={'secondary'}
|
||||
disabled={readonly}
|
||||
on:click={selectIcon}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<ModernButton
|
||||
icon={IconSquareExpand}
|
||||
@ -154,12 +162,15 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AttributeEditor
|
||||
_class={task.class.TaskType}
|
||||
object={taskType}
|
||||
key="name"
|
||||
editKind={'modern-ghost-large'}
|
||||
/>
|
||||
<div class="name" class:editable={!readonly}>
|
||||
<AttributeEditor
|
||||
_class={task.class.TaskType}
|
||||
object={taskType}
|
||||
key="name"
|
||||
editKind={'modern-ghost-large'}
|
||||
editable={!readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center mt-4 ml-4 mr-4 gap-4">
|
||||
<div class="flex-no-shrink trans-title uppercase">
|
||||
@ -185,12 +196,19 @@
|
||||
<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} />
|
||||
<ButtonIcon
|
||||
kind={'primary'}
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
on:click={handleAddStatus}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
<StatesProjectEditor
|
||||
{taskType}
|
||||
type={spaceType}
|
||||
{states}
|
||||
{readonly}
|
||||
on:delete={async (evt) => {
|
||||
if (taskType === undefined) {
|
||||
return
|
||||
@ -207,7 +225,7 @@
|
||||
})
|
||||
}}
|
||||
on:move={async (evt) => {
|
||||
if (taskType === undefined) {
|
||||
if (taskType === undefined || readonly) {
|
||||
return
|
||||
}
|
||||
const index = taskType.statuses.findIndex((p) => p === evt.detail.stateID)
|
||||
@ -229,9 +247,24 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ClassAttributes ofClass={taskType.ofClass} _class={taskType.targetClass} showHierarchy />
|
||||
<ClassAttributes ofClass={taskType.ofClass} _class={taskType.targetClass} showHierarchy disabled={readonly} />
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.name {
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
|
||||
&.editable {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { TaskTypeKind } from '@hcengineering/task'
|
||||
import { Label, ButtonMenu } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
@ -24,6 +24,7 @@
|
||||
export let selected: string | undefined = undefined
|
||||
export let key: 'color' | 'icon' = 'color'
|
||||
export let embedded: boolean = false
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
@ -36,9 +37,11 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="color"
|
||||
class:disabled
|
||||
class:selected={selected === color.name}
|
||||
style="background-color: {col}"
|
||||
on:click={() => {
|
||||
if (disabled) return
|
||||
dispatch('close', i)
|
||||
}}
|
||||
/>
|
||||
@ -53,9 +56,11 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="color"
|
||||
class:disabled
|
||||
class:selected={selected === color.name}
|
||||
style="background-color: {col}"
|
||||
on:click={() => {
|
||||
if (disabled) return
|
||||
dispatch('close', i)
|
||||
}}
|
||||
/>
|
||||
@ -89,6 +94,9 @@
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 32 32'%3E%3Cpath fill='%23FFFFFF' d='M23.6,10.9c-0.6-0.6-1.5-0.6-2.1,0l-6.9,6.9l-3.4-3.4c-0.6-0.6-1.5-0.6-2.1,0c-0.6,0.6-0.6,1.5,0,2.1l5.6,5.6l9.1-9.1C24.1,12.5,24.1,11.5,23.6,10.9z'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
.color-grid {
|
||||
|
@ -38,14 +38,14 @@ test.describe('contact tests', () => {
|
||||
await templatePage.editTemplate('some more2 value')
|
||||
})
|
||||
|
||||
test('manage-templates', async () => {
|
||||
// TODO: Need rework.
|
||||
test.skip('manage-templates', async () => {
|
||||
await templatePage.navigateToWorkspace(platformUri)
|
||||
await templatePage.openProfileMenu()
|
||||
await templatePage.openSettings()
|
||||
await templatePage.goToNotifications()
|
||||
await templatePage.selectVacancies()
|
||||
|
||||
// TODO: Need rework.
|
||||
// await page.getByRole('button', { name: 'Recruiting', exact: true }).click()
|
||||
// await page.locator('#navGroup-statuses').getByText('New Recruiting project type').first().click()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user