[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:
Alexey Zinoviev 2024-05-05 18:18:30 +04:00 committed by GitHub
parent bb29cebdd9
commit 3cbec84e5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 396 additions and 117 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -112,6 +112,8 @@
"Roles": "Роли",
"RoleName": "Название роли",
"Permissions": "Разрешения",
"Assignees": "Назначенные"
"Assignees": "Назначенные",
"DeleteRole": "Удалить роль",
"DeleteRoleConfirmation": "Вы действительно хотите удалить эту роль? Все пользователи с этой ролью потеряют имеющиеся разрешения."
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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