Skip to content

Commit 30a54e5

Browse files
authored
KB : optional icon (#24290)
1 parent 6def56c commit 30a54e5

17 files changed

Lines changed: 429 additions & 98 deletions

File tree

js/modules/IllustrationPicker/Controller.js

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -216,39 +216,49 @@ export class GlpiIllustrationPickerController
216216
{
217217
// Gets details of the newly selected item.
218218
const illustration_id = illustration.dataset['glpiIconPickerValue'];
219-
const illustration_title = illustration
220-
.querySelector('svg')
221-
.querySelector('title')
222-
;
223219

224220
// Apply the new illustration id to the hidden input.
225221
this.#getSelectedIllustrationsInput().value = illustration_id;
226222

227-
// Update the preview of the selected item.
228-
const selected_svg = this.#container
229-
.querySelector('[data-glpi-icon-picker-value-preview]')
230-
.querySelector('svg')
231-
;
232-
const title = selected_svg.querySelector('title');
233-
const use = selected_svg.querySelector('use');
234-
const xlink = use.getAttribute('xlink:href');
223+
if (illustration_id === '') {
224+
// Empty selection: show placeholder, hide both preview slots.
225+
this.#setEmptyPreview();
226+
return;
227+
}
235228

236-
use.setAttribute(
237-
'xlink:href',
238-
xlink.replace(/#.*/, `#${illustration_id}`)
239-
);
240-
title.innerHTML = illustration_title.innerHTML;
229+
// Clone the clicked illustration's SVG into the native preview slot.
230+
// Cloning (vs in-place mutation) handles the transition from an empty
231+
// article — where the slot has no SVG to mutate — without requiring
232+
// the template to pre-render a skeleton SVG.
233+
const native_slot = this.#getNativePreviewSlot();
234+
const clicked_svg = illustration.querySelector('svg');
235+
native_slot.replaceChildren(clicked_svg.cloneNode(true));
236+
237+
native_slot.classList.remove('d-none');
238+
this.#getCustomPreviewSlot().classList.add('d-none');
239+
this.#getPlaceholderSlot()?.classList.add('d-none');
240+
}
241241

242-
this.#container
243-
.querySelector('[data-glpi-icon-picker-value-preview-native]')
244-
.classList
245-
.remove('d-none')
246-
;
247-
this.#container
248-
.querySelector('[data-glpi-icon-picker-value-preview-custom]')
249-
.classList
250-
.add('d-none')
251-
;
242+
#setEmptyPreview()
243+
{
244+
this.#getNativePreviewSlot()?.classList.add('d-none');
245+
this.#getCustomPreviewSlot()?.classList.add('d-none');
246+
this.#getPlaceholderSlot()?.classList.remove('d-none');
247+
}
248+
249+
#getNativePreviewSlot()
250+
{
251+
return this.#container.querySelector('[data-glpi-icon-picker-value-preview-native]');
252+
}
253+
254+
#getCustomPreviewSlot()
255+
{
256+
return this.#container.querySelector('[data-glpi-icon-picker-value-preview-custom]');
257+
}
258+
259+
#getPlaceholderSlot()
260+
{
261+
return this.#container.querySelector('[data-glpi-icon-picker-value-preview-placeholder]');
252262
}
253263

254264
#setCustomIllustration(file_id)
@@ -257,21 +267,12 @@ export class GlpiIllustrationPickerController
257267
this.#getSelectedIllustrationsInput().value = icon_id;
258268

259269
// Update preview
260-
this.#container
261-
.querySelector('[data-glpi-icon-picker-value-preview-custom]')
262-
.querySelector('img')
263-
.src = `${CFG_GLPI.root_doc}/UI/Illustration/CustomIllustration/${file_id}`
264-
;
265-
this.#container
266-
.querySelector('[data-glpi-icon-picker-value-preview-custom]')
267-
.classList
268-
.remove('d-none')
269-
;
270-
this.#container
271-
.querySelector('[data-glpi-icon-picker-value-preview-native]')
272-
.classList
273-
.add('d-none')
274-
;
270+
const custom_slot = this.#getCustomPreviewSlot();
271+
custom_slot.querySelector('img').src
272+
= `${CFG_GLPI.root_doc}/UI/Illustration/CustomIllustration/${file_id}`;
273+
custom_slot.classList.remove('d-none');
274+
this.#getNativePreviewSlot().classList.add('d-none');
275+
this.#getPlaceholderSlot()?.classList.add('d-none');
275276
}
276277

277278
/**

js/modules/Knowbase/ArticleController.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -858,13 +858,27 @@ export class GlpiKnowbaseArticleController
858858

859859
#setIllustrationEditable(editable)
860860
{
861-
const picker = this.#container.querySelector(
862-
'[data-glpi-kb-illustration-container] [data-glpi-illustration-picker]'
861+
const container = this.#container.querySelector(
862+
'[data-glpi-kb-illustration-container]'
863863
);
864+
const picker = container?.querySelector('[data-glpi-illustration-picker]');
864865
if (!picker) {
865866
return;
866867
}
867868

869+
// Reveal the container when entering edit mode so the user can pick an
870+
// illustration even when none was previously set. When leaving edit
871+
// mode with an empty value, hide the container again so the title
872+
// realigns to the left.
873+
if (editable) {
874+
container.classList.remove('d-none');
875+
} else {
876+
const input = picker.querySelector('[data-glpi-icon-picker-value]');
877+
if (!input?.value) {
878+
container.classList.add('d-none');
879+
}
880+
}
881+
868882
if (picker.glpiIllustrationPicker) {
869883
picker.glpiIllustrationPicker.setEditable(editable);
870884
return;
@@ -1121,7 +1135,7 @@ export class GlpiKnowbaseArticleController
11211135
'[data-glpi-kb-illustration-container] [data-glpi-icon-picker-value]'
11221136
);
11231137
const illustration = illustration_input?.value ?? '';
1124-
if (illustration && illustration !== 'kb-faq') {
1138+
if (illustration) {
11251139
fields.illustration = illustration;
11261140
}
11271141

src/Glpi/Knowbase/Aside/Builder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private function populateNode(Node $node, int $category_id): void
6262
$node->addArticle(new Article(
6363
id: (int) $article_data['id'],
6464
title: $article_data['name'] ?? '',
65-
illustration: $article_data['illustration'] ?? 'kb-faq',
65+
illustration: $article_data['illustration'] ?? '',
6666
link: KnowbaseItem::getFormURLWithID($article_data['id']),
6767
is_current: $this->current_id > 0 && (int) $article_data['id'] === $this->current_id,
6868
));
@@ -74,7 +74,7 @@ private function populateNode(Node $node, int $category_id): void
7474
foreach ($categories as $cat_data) {
7575
$category = new Category(
7676
title: $cat_data['name'] ?? '',
77-
illustration: $cat_data['illustration'] ?: 'kb-faq',
77+
illustration: $cat_data['illustration'] ?? '',
7878
);
7979
$this->populateNode($category, (int) $cat_data['id']);
8080
$node->addCategory($category);

src/Glpi/Knowbase/History/HistoryBuilder.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -505,14 +505,16 @@ private function addIllustrationChangesToHistory(): void
505505
]);
506506

507507
foreach ($logs as $row) {
508-
$is_custom = str_starts_with(
508+
if ($row['new_value'] === '') {
509+
$description = __("Illustration removed by");
510+
} elseif (str_starts_with(
509511
$row['new_value'],
510512
IllustrationManager::CUSTOM_ILLUSTRATION_PREFIX
511-
);
512-
$description = $is_custom
513-
? __("Custom illustration set by")
514-
: __("Native illustration set by")
515-
;
513+
)) {
514+
$description = __("Custom illustration set by");
515+
} else {
516+
$description = __("Native illustration set by");
517+
}
516518

517519
$this->history->addEvent(new LogEvent(
518520
label: __("Illustration updated"),

src/Glpi/UI/IllustrationManager.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ public function getCustomIconIdFromPrefixedString(string $prefixed_id): string
109109
*/
110110
public function renderIcon(string $icon_id, ?int $size = null): string
111111
{
112+
if ($icon_id === '') {
113+
return '';
114+
}
115+
112116
$custom_icon_prefix = self::CUSTOM_ILLUSTRATION_PREFIX;
113117
if (str_starts_with($icon_id, $custom_icon_prefix)) {
114118
return $this->renderCustomIcon(
@@ -147,6 +151,24 @@ public function getAllIconsIds(): array
147151
return array_keys($this->getIconsDefinitions());
148152
}
149153

154+
/**
155+
* Tell whether a string is a valid illustration value: empty (no
156+
* illustration), a known native icon id, or an existing custom file.
157+
*/
158+
public function isKnownIllustrationValue(string $value): bool
159+
{
160+
if ($value === '') {
161+
return true;
162+
}
163+
164+
if (str_starts_with($value, self::CUSTOM_ILLUSTRATION_PREFIX)) {
165+
$custom_id = substr($value, strlen(self::CUSTOM_ILLUSTRATION_PREFIX));
166+
return $this->getCustomIllustrationFile($custom_id) !== null;
167+
}
168+
169+
return in_array($value, $this->getAllIconsIds(), true);
170+
}
171+
150172
public function countIcons(string $filter = ""): int
151173
{
152174
if ($filter == "") {

src/KnowbaseItem.php

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -899,23 +899,8 @@ private function prepareIllustrationInput(array $input): array
899899
return $input;
900900
}
901901

902-
$value = (string) $input['illustration'];
903-
if ($value === '') {
904-
return $input;
905-
}
906-
907902
$manager = new IllustrationManager();
908-
$prefix = IllustrationManager::CUSTOM_ILLUSTRATION_PREFIX;
909-
910-
if (str_starts_with($value, $prefix)) {
911-
$custom_id = substr($value, strlen($prefix));
912-
if ($manager->getCustomIllustrationFile($custom_id) === null) {
913-
unset($input['illustration']);
914-
}
915-
return $input;
916-
}
917-
918-
if (!in_array($value, $manager->getAllIconsIds(), true)) {
903+
if (!$manager->isKnownIllustrationValue((string) $input['illustration'])) {
919904
unset($input['illustration']);
920905
}
921906

@@ -1047,7 +1032,7 @@ public function showFull($options = [])
10471032
// General fields
10481033
$params['views'] = $this->fields['view'];
10491034
$params['can_edit'] = $can_update;
1050-
$params['illustration'] = $this->fields['illustration'] ?: 'kb-faq';
1035+
$params['illustration'] = $this->fields['illustration'] ?? '';
10511036

10521037
// Translations informations
10531038
$params['translations_count'] = KnowbaseItemTranslation::getNumberOfTranslationsForItem($this);
@@ -1068,7 +1053,7 @@ public function showFull($options = [])
10681053
$params['actions'] = $this->getEditorActions();
10691054
} elseif ($mode === "add") {
10701055
$params['can_edit'] = $this->can(-1, CREATE);
1071-
$params['illustration'] = 'kb-faq';
1056+
$params['illustration'] = '';
10721057
}
10731058

10741059
$out = TemplateRenderer::getInstance()->render(
@@ -2833,7 +2818,7 @@ private function getCurrentArticleAndFavorites(int $current_id = 0): array
28332818
$articles[] = new Article(
28342819
id: (int) $data['id'],
28352820
title: $data['name'] ?? '',
2836-
illustration: $data['illustration'] ?: 'kb-faq',
2821+
illustration: $data['illustration'] ?? '',
28372822
link: self::getFormURLWithID($data['id']),
28382823
// Take note of the current article as we will render it in
28392824
// the favorite list even if it is not yet a favorite.

src/KnowbaseItemCategory.php

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
* ---------------------------------------------------------------------
3434
*/
3535

36+
use Glpi\UI\IllustrationManager;
37+
3638
/// Class KnowbaseItemCategory
3739
class KnowbaseItemCategory extends CommonTreeDropdown
3840
{
@@ -71,14 +73,55 @@ public function getAdditionalFields()
7173
$fields = parent::getAdditionalFields();
7274

7375
$fields[] = [
74-
'name' => 'illustration',
75-
'type' => 'illustration',
76-
'label' => __('Illustration'),
76+
'name' => 'illustration',
77+
'type' => 'illustration',
78+
'label' => __('Illustration'),
79+
'default_illustration' => '',
7780
];
7881

7982
return $fields;
8083
}
8184

85+
public function prepareInputForAdd($input)
86+
{
87+
$input = parent::prepareInputForAdd($input);
88+
if (!is_array($input)) {
89+
return $input;
90+
}
91+
return $this->prepareIllustrationInput($input);
92+
}
93+
94+
public function prepareInputForUpdate($input)
95+
{
96+
$input = parent::prepareInputForUpdate($input);
97+
if (!is_array($input)) {
98+
return $input;
99+
}
100+
return $this->prepareIllustrationInput($input);
101+
}
102+
103+
/**
104+
* Drop the `illustration` field from $input when it is neither a known
105+
* native icon nor an existing custom illustration file. The picker UI
106+
* always submits valid values; this guards against direct POSTs.
107+
*
108+
* @param array<string, mixed> $input
109+
* @return array<string, mixed>
110+
*/
111+
private function prepareIllustrationInput(array $input): array
112+
{
113+
if (!array_key_exists('illustration', $input)) {
114+
return $input;
115+
}
116+
117+
$manager = new IllustrationManager();
118+
if (!$manager->isKnownIllustrationValue((string) $input['illustration'])) {
119+
unset($input['illustration']);
120+
}
121+
122+
return $input;
123+
}
124+
82125
public function cleanDBonPurge()
83126
{
84127
$this->deleteChildrenAndRelationsFromDb(

templates/components/illustration/icon_picker.html.twig

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,25 +84,36 @@
8484
>
8585
<div class="{{ preview_inner_css_classes }}">
8686
{% set is_custom_file = value starts with custom_icon_prefix %}
87+
{% set is_empty = value is empty %}
88+
89+
{# Placeholder: visible when no illustration is set. #}
90+
<div
91+
class="d-flex align-items-center justify-content-center{% if not is_empty %} d-none{% endif %}"
92+
data-glpi-icon-picker-value-preview-placeholder
93+
style="width: {{ size }}px; height: {{ size }}px;"
94+
>
95+
<i class="ti ti-photo-plus text-muted" style="font-size: {{ (size * 0.6)|round }}px;" aria-hidden="true"></i>
96+
</div>
97+
98+
{# Native illustration preview slot. #}
8799
<div
88-
{% if is_custom_file %}
89-
data-glpi-icon-picker-value-preview-custom
90-
data-testid="illustration-custom-preview"
91-
{% else %}
92-
data-glpi-icon-picker-value-preview-native
100+
class="{% if is_custom_file or is_empty %}d-none{% endif %}"
101+
data-glpi-icon-picker-value-preview-native
102+
style="width: {{ size }}px; height: {{ size }}px;"
103+
>
104+
{% if not is_custom_file and not is_empty %}
105+
{{ render_illustration(value, size) }}
93106
{% endif %}
107+
</div>
108+
109+
{# Custom illustration preview slot. #}
110+
<div
111+
class="{% if not is_custom_file %}d-none{% endif %}"
112+
data-glpi-icon-picker-value-preview-custom
113+
data-testid="illustration-custom-preview"
94114
>
95-
{{ render_illustration(value, size) }}
115+
{{ render_illustration(is_custom_file ? value : custom_icon_prefix, size) }}
96116
</div>
97-
{% if is_custom_file %}
98-
<div class="d-none" data-glpi-icon-picker-value-preview-native>
99-
{{ render_illustration('', size) }}
100-
</div>
101-
{% else %}
102-
<div class="d-none" data-glpi-icon-picker-value-preview-custom data-testid="illustration-custom-preview">
103-
{{ render_illustration(custom_icon_prefix, size) }}
104-
</div>
105-
{% endif %}
106117
</div>
107118
</div>
108119

0 commit comments

Comments
 (0)