Ability to show ids in title (#10318)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-12-19 20:26:53 +05:00
committed by GitHub
parent bc0a3e15cb
commit 33bb180505
31 changed files with 110 additions and 42 deletions
@@ -344,6 +344,7 @@ export interface TypeDate extends Type<Date> {
*/
export interface TypeIdentifier extends Type<string> {
of: Ref<CustomSequence>
showInPresenter?: boolean
}
/**
+2 -2
View File
@@ -472,8 +472,8 @@ export function TypeRef (_class: Ref<Class<Doc>>): RefTo<Doc> {
/**
* @public
*/
export function TypeIdentifier (of: Ref<CustomSequence>): TypeIdentifierType {
return { _class: core.class.TypeIdentifier, label: core.string.Id, icon: core.icon.TypeRef, of }
export function TypeIdentifier (of: Ref<CustomSequence>, showInPresenter: boolean): TypeIdentifierType {
return { _class: core.class.TypeIdentifier, label: core.string.Id, icon: core.icon.TypeRef, of, showInPresenter }
}
/**
@@ -34,7 +34,7 @@
export let withIcon: boolean = false
export let withSelectIcon: boolean = true
export let disableFocusOnMouseover: boolean = false
export let withSearch: boolean = false
export let withSearch: boolean = true
let container: HTMLElement
let opened: boolean = false
@@ -59,7 +59,7 @@
})
function canChange (attr: AnyAttribute, permissionsStore: PermissionsStore): boolean {
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore)
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore, _class)
}
</script>
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Card, MasterTag } from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import core, { Ref, TypeIdentifier } from '@hcengineering/core'
import { Asset, getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, tooltip } from '@hcengineering/ui'
@@ -56,6 +56,25 @@
$: _class = cardObj && (client.getHierarchy().getClass(cardObj?._class) as MasterTag)
$: icon = _class && _class.icon
$: ids = getIds(cardObj)
function getIds (val: Card | undefined): string {
if (val === undefined) return ''
const h = client.getHierarchy()
const attrs = h.getAllAttributes(val._class, core.class.Doc)
const res: string[] = []
for (const [k, v] of attrs) {
if (v.type._class === core.class.TypeIdentifier) {
const type = v.type as TypeIdentifier
const str = (val as any)[k]
if (type.showInPresenter === true && str !== undefined) {
res.push(str)
}
}
}
return res.join(' ')
}
</script>
{#if inline && cardObj}
@@ -82,6 +101,7 @@
</div>
{/if}
<span class="overflow-label">
{ids}
{cardObj.title}
<slot name="details" />
</span>
@@ -106,6 +126,7 @@
</div>
{/if}
<span class="overflow-label cropped-text-presenter">
{ids}
{cardObj.title}
<slot name="details" />
</span>
@@ -113,6 +134,7 @@
{/if}
{:else}
<span class="overflow-label" class:select-text={!noSelect} use:tooltip={{ label: getEmbeddedLabel(cardObj.title) }}>
{ids}
{cardObj.title}
</span>
{/if}
@@ -15,7 +15,7 @@
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import { IconWithEmoji, createQuery } from '@hcengineering/presentation'
import { IconWithEmoji, createQuery, getClient } from '@hcengineering/presentation'
import { clearSettingsStore } from '@hcengineering/setting-resources'
import {
Icon,
@@ -40,10 +40,16 @@
selectedTagId = loc.path[4] as Ref<MasterTag>
}
const client = getClient()
const h = client.getHierarchy()
let tags: MasterTag[] = []
const tagsQuery = createQuery()
$: tagsQuery.query(card.class.MasterTag, {}, (result) => {
tags = result.filter((p) => p.removed !== true).sort((a, b) => a.label.localeCompare(b.label))
tags = [
h.getClass(card.class.Card),
...result.filter((p) => p.removed !== true).sort((a, b) => a.label.localeCompare(b.label))
]
})
function selectProjectType (id: string): void {
-1
View File
@@ -92,7 +92,6 @@ export default mergeIds(cardId, card, {
string: {
CreateMasterTag: '' as IntlString,
CreateTag: '' as IntlString,
CreateCard: '' as IntlString,
Content: '' as IntlString,
Parent: '' as IntlString,
CardLibrary: '' as IntlString,
+2 -1
View File
@@ -191,7 +191,8 @@ const cardPlugin = plugin(cardId, {
AddTag: '' as IntlString,
Feed: '' as IntlString,
AllCards: '' as IntlString,
Favorites: '' as IntlString
Favorites: '' as IntlString,
CreateCard: '' as IntlString
},
section: {
Attachments: '' as Ref<CardSection>,
@@ -112,7 +112,7 @@
</script>
<Modal
label={getEmbeddedLabel('Create card from message')}
label={cardPlugin.string.CreateCard}
type="type-popup"
okLabel={presentation.string.Create}
okAction={attachCard}
@@ -127,10 +127,10 @@
<div class="hulyModal-content__settingsSet">
<div class="hulyModal-content__settingsSet-line">
<span class="label"><Label label={getEmbeddedLabel('Card type')} /></span>
<span class="label"><Label label={cardPlugin.string.MasterTag} /></span>
<NestedDropdown
items={classes}
label={getEmbeddedLabel('Card type')}
label={cardPlugin.string.MasterTag}
on:selected={(e) => {
selectedType = e.detail
}}
+2 -1
View File
@@ -209,6 +209,7 @@
"DefaultStartWithRecording": "Povolit záznam v nových kancelářích",
"ImportDocumentPermission": "Importovat dokumenty",
"ImportDocumentDescription": "Umožňuje uživatelům importovat dokumenty do pracovního prostoru",
"SelectUsers": "Vybrat uživatele"
"SelectUsers": "Vybrat uživatele",
"ShowInTitle": "Zobrazit v názvu"
}
}
+2 -1
View File
@@ -211,6 +211,7 @@
"DefaultStartWithRecording": "Aufnahme in neuen Büroräumen aktivieren",
"ImportDocumentPermission": "Dokumente importieren",
"ImportDocumentDescription": "Gewährt Benutzern die Möglichkeit, Dokumente in den Arbeitsbereich zu importieren",
"SelectUsers": "Benutzer auswählen"
"SelectUsers": "Benutzer auswählen",
"ShowInTitle": "Im Titel anzeigen"
}
}
+2 -1
View File
@@ -211,6 +211,7 @@
"DefaultStartWithRecording": "Enable recording in new office rooms",
"ImportDocumentPermission": "Import documents",
"ImportDocumentDescription": "Grants users ability to import documents into the workspace",
"SelectUsers": "Select users"
"SelectUsers": "Select users",
"ShowInTitle": "Show in title"
}
}
+2 -1
View File
@@ -202,6 +202,7 @@
"DefaultStartWithRecording": "Habilitar grabación en nuevas oficinas",
"ImportDocumentPermission": "Importar documentos",
"ImportDocumentDescription": "Otorga a los usuarios la capacidad de importar documentos al espacio de trabajo",
"SelectUsers": "Seleccionar usuarios"
"SelectUsers": "Seleccionar usuarios",
"ShowInTitle": "Mostrar en el título"
}
}
+2 -1
View File
@@ -211,6 +211,7 @@
"DefaultStartWithRecording": "Activer l'enregistrement dans les nouveaux bureaux",
"ImportDocumentPermission": "Importer des documents",
"ImportDocumentDescription": "Accorde aux utilisateurs la possibilité d'importer des documents dans l'espace de travail",
"SelectUsers": "Sélectionner des utilisateurs"
"SelectUsers": "Sélectionner des utilisateurs",
"ShowInTitle": "Afficher dans le titre"
}
}
+2 -1
View File
@@ -211,6 +211,7 @@
"DefaultStartWithRecording": "Abilita registrazione nelle nuove stanze dell'ufficio",
"ImportDocumentPermission": "Importa documenti",
"ImportDocumentDescription": "Concede agli utenti la possibilità di importare documenti nell'area di lavoro",
"SelectUsers": "Seleziona utenti"
"SelectUsers": "Seleziona utenti",
"ShowInTitle": "Mostra nel titolo"
}
}
+2 -1
View File
@@ -211,6 +211,7 @@
"DefaultStartWithRecording": "新しいオフィスルームで録画を有効にする",
"ImportDocumentPermission": "ドキュメントをインポート",
"ImportDocumentDescription": "ユーザーにワークスペースにドキュメントをインポートする機能を付与します",
"SelectUsers": "ユーザーを選択"
"SelectUsers": "ユーザーを選択",
"ShowInTitle": "タイトルに表示"
}
}
+2 -1
View File
@@ -202,6 +202,7 @@
"DefaultStartWithRecording": "Habilitar gravação em novos escritórios",
"ImportDocumentPermission": "Importar documentos",
"ImportDocumentDescription": "Concede aos usuários a capacidade de importar documentos para o espaço de trabalho",
"SelectUsers": "Selecionar usuários"
"SelectUsers": "Selecionar usuários",
"ShowInTitle": "Mostrar no título"
}
}
+2 -1
View File
@@ -211,6 +211,7 @@
"DefaultStartWithRecording": "Включить запись в новых комнатах",
"ImportDocumentPermission": "Импорт документов",
"ImportDocumentDescription": "Предоставляет пользователям возможность импортировать документы в рабочее пространство",
"SelectUsers": "Выбрать пользователей"
"SelectUsers": "Выбрать пользователей",
"ShowInTitle": "Показывать в заголовке"
}
}
+2 -1
View File
@@ -211,6 +211,7 @@
"DefaultStartWithRecording": "Yeni ofis odalarında kaydı etkinleştir",
"ImportDocumentPermission": "Belgeleri içe aktar",
"ImportDocumentDescription": "Kullanıcılara çalışma alanına belge içe aktarma yeteneği verir",
"SelectUsers": "Kullanıcıları seç"
"SelectUsers": "Kullanıcıları seç",
"ShowInTitle": "Başlıkta göster"
}
}
+2 -1
View File
@@ -211,6 +211,7 @@
"DefaultStartWithRecording": "在新办公室启用录制",
"ImportDocumentPermission": "导入文档",
"ImportDocumentDescription": "授予用户将文档导入工作区的权限",
"SelectUsers": "选择用户"
"SelectUsers": "选择用户",
"ShowInTitle": "在标题中显示"
}
}
@@ -162,6 +162,7 @@
{:else}
<NestedDropdown
items={classes}
withSearch
on:selected={(e) => {
classARef = e.detail
}}
@@ -192,6 +193,7 @@
<div>
<NestedDropdown
items={classes}
withSearch
on:selected={(e) => {
classBRef = e.detail
}}
@@ -134,6 +134,12 @@
defaultValue = e.detail?.defaultValue
}
const onTypePatch = async (type: Type<any>) => {
if (type !== undefined && type._class === attribute.type._class) {
await client.updateDoc(attribute._class, attribute.space, attribute._id, { type })
}
}
async function remove (evt: MouseEvent): Promise<void> {
const impl = await getResource(view.actionImpl.Delete)
await impl(attribute, evt, {
@@ -165,7 +171,7 @@
return `${attr._id}_${forbidden ? 'forbidden' : 'allowed'}` as Ref<AttributePermission>
}
function changeRestricted (e: CustomEvent<boolean>): void {
function changeRestricted (): void {
showPopup(
MessageBox,
{
@@ -285,7 +291,8 @@
width: '100%',
editable: !exist && !disabled,
kind: 'regular',
size: 'large'
size: 'large',
onTypePatch
}}
{disabled}
on:change={handleChange}
@@ -13,15 +13,16 @@
// limitations under the License.
-->
<script lang="ts">
import core, { CustomSequence, Ref, TypeIdentifier as TypeId } from '@hcengineering/core'
import core, { CustomSequence, Ref, Type, TypeIdentifier as TypeId } from '@hcengineering/core'
import { TypeIdentifier } from '@hcengineering/model'
import { createQuery, getClient } from '@hcengineering/presentation'
import { EditBox, Label } from '@hcengineering/ui'
import { EditBox, Label, Toggle } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import setting from '../../plugin'
export let type: TypeId | undefined
export let editable: boolean = true
export let onTypePatch: ((type: Type<any>) => Promise<void>) | undefined = undefined
const dispatch = createEventDispatcher()
const client = getClient()
@@ -30,6 +31,7 @@
let identifier: string = ''
let sequences: CustomSequence[] = []
let showInPresenter = type?.showInPresenter ?? false
$: identifiers = new Set(sequences.filter((p) => p._id !== seq).map((s) => s.prefix.toUpperCase()))
@@ -54,7 +56,16 @@
attachedTo: core.class.CustomSequence
})
seq = newSeq
dispatch('change', { type: TypeIdentifier(newSeq) })
dispatch('change', { type: TypeIdentifier(newSeq, showInPresenter) })
}
}
async function changeShowing () {
if (seq === undefined) return
const type = TypeIdentifier(seq, showInPresenter)
dispatch('change', { type })
if (!editable) {
onTypePatch?.(type)
}
}
</script>
@@ -77,6 +88,10 @@
</div>
{/if}
</div>
<span class="label">
<Label label={setting.string.ShowInTitle} />
</span>
<Toggle bind:on={showInPresenter} on:change={changeShowing} />
<style lang="scss">
.duplicated-identifier {
@@ -41,12 +41,7 @@
function fillClasses (classes: Ref<Class<Doc>>[], exclude: Ref<Class<Doc>>[]): DropdownIntlItem[] {
const res: DropdownIntlItem[] = []
const descendants = new Set(
classes
.map((p) => hierarchy.getDescendants(p))
.reduce((a, b) => a.concat(b))
.filter((p) => p !== card.class.Card)
)
const descendants = new Set(classes.map((p) => hierarchy.getDescendants(p)).reduce((a, b) => a.concat(b)))
// exclude removed card types
const removedTypes = client.getModel().findAllSync(card.class.MasterTag, { removed: true })
const excluded = new Set(removedTypes.map((p) => p._id))
+2 -1
View File
@@ -155,6 +155,7 @@ export default mergeIds(settingId, setting, {
Restricted: '' as IntlString,
RestrictedAttributeWarning: '' as IntlString,
PasswordAgingRule: '' as IntlString,
PasswordAgingRuleDescription: '' as IntlString
PasswordAgingRuleDescription: '' as IntlString,
ShowInTitle: '' as IntlString
}
})
@@ -168,7 +168,7 @@
): boolean {
if (attr === undefined) return true
if (permissionsStore === undefined) return true
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore)
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore, object._class)
}
</script>
@@ -356,7 +356,7 @@
): boolean {
if (permissionsStore === undefined) return true
if (attr === undefined) return true
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore)
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore, object._class)
}
async function canEdit (object: Doc): Promise<boolean> {
@@ -68,7 +68,7 @@
): boolean {
if (attr === undefined) return true
if (permissionsStore === undefined) return true
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore)
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore, object._class)
}
</script>
+12 -2
View File
@@ -15,7 +15,7 @@ export function canChangeAttribute (
attr: AnyAttribute,
space: Ref<TypedSpace>,
store: PermissionsStore,
_class?: Ref<Class<Doc>>
_class: Ref<Class<Doc>>
): boolean {
const arePermissionsDisabled = getMetadata(core.metadata.DisablePermissions) ?? false
if (arePermissionsDisabled) return true
@@ -43,7 +43,7 @@ export function canChangeAttribute (
return true
}
return canChangeDoc(_class ?? attr.attributeOf, space, store)
return canChangeDoc(_class, space, store)
}
export function canChangeDoc (_class: Ref<Class<Doc>>, space: Ref<Space>, store: PermissionsStore): boolean {
@@ -51,6 +51,16 @@ export function canChangeDoc (_class: Ref<Class<Doc>>, space: Ref<Space>, store:
if (arePermissionsDisabled) return true
if (store.whitelist.has(space)) return true
if (store.ps[space] !== undefined) {
const forbiddenClId = `${_class}_forbidden` as Ref<Permission>
const forbiddenCl = store.ps[space]?.has(forbiddenClId)
if (forbiddenCl) {
return false
}
const allowedClId = `${_class}_allowed` as Ref<Permission>
const allowedCl = store.ps[space]?.has(allowedClId)
if (allowedCl) {
return true
}
const client = getClient()
const h = client.getHierarchy()
const ancestors = h.getAncestors(_class)
@@ -157,7 +157,6 @@ export class CustomAttributesPage {
await expect(this.lead().nth(1)).toBeVisible()
await expect(this.issue().nth(0)).toBeVisible()
await expect(this.issue().nth(1)).toBeVisible()
await expect(this.card()).toBeVisible()
await expect(this.product()).toBeVisible()
}
}
@@ -53,7 +53,6 @@ export class ClassesPage {
await expect(this.lead().nth(1)).toBeVisible()
await expect(this.issue().nth(0)).toBeVisible()
await expect(this.issue().nth(1)).toBeVisible()
await expect(this.card()).toBeVisible()
await expect(this.product()).toBeVisible()
}
}