Skip to content

Commit c6e5df4

Browse files
engijlrnielslyngsoemadsrasmussen
authored
Backoffice: Add Entity Signs (overlay icons) to tree items. (#20328)
* entity signs folder * update package.json * entity sign extension type * implement entity sign extension * POC document has collection sign * implement icon kind * rename file * note about this being wrong * move type * change import * entity sign bundle element * implement icon kind label * Display icon and show popover on hover * Fix the popover logic * Moving the sign icon to the iconContainer to handle position * fix missing document tree icon * revert removal of icon slot render * remove unused styles * document tree item - inherit styles from the base element * correctly extend styles * revert document tree item icon change * move icon container html * add method to get an icon name * Adding delay to the popover when opens * Add animation to popover when it opens * Making the parent of the entity bundle trigger popover on hover * Display 2 icons over the main icon * Updating some styles * Position one icon on top of the other and add css style variables * Changing popover-container for position-anchor * generate server types * Using css properties to display and animate the signs * Stacked icons using grid property * Use translate property to move the icons around * Added fallback styles for firefox * formatting of state properties * implement entity flags across content types * lint fixes * fix import extension mess * await both properties for this to work * transfer flags to entity sign bundle ext initializer * is-protected entity sign * Made signs infobox show downward. * Changed px to rems * Change the manifest for the actual signs we will display * add icon color, remove unused label, add weight * changes styles + animation + slotted icon inside * Overwrite pending changes when schedule is active and added green color to schedule. * adjust animation * add background for sign * avoid re-rendering when properties are being set * Bind the flags to each sign manifest. * increase signs offset * fix document tree item draft style * Removed unused exports. * Remove duplicated hover timer logic. * Added eslint disable line to keep the empty method for future implementation. * rename class * Rename interface for optional entity flags * make alias more explicit to prevent future collisions * include alias in field name to make it clear that we do not except all colors * align function names with conventions * always include flags in document items * compose tree types * set up entity-flag module and move related types * change label --------- Co-authored-by: Niels Lyngsø <[email protected]> Co-authored-by: Mads Rasmussen <[email protected]>
1 parent 193d8af commit c6e5df4

File tree

76 files changed

+771
-141
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+771
-141
lines changed

src/Umbraco.Web.UI.Client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"./entity-bulk-action": "./dist-cms/packages/core/entity-bulk-action/index.js",
4949
"./entity-create-option-action": "./dist-cms/packages/core/entity-create-option-action/index.js",
5050
"./entity-item": "./dist-cms/packages/core/entity-item/index.js",
51+
"./entity-sign": "./dist-cms/packages/core/entity-sign/index.js",
52+
"./entity-flag": "./dist-cms/packages/core/entity-flag/index.js",
5153
"./entity": "./dist-cms/packages/core/entity/index.js",
5254
"./event": "./dist-cms/packages/core/event/index.js",
5355
"./extension-registry": "./dist-cms/packages/core/extension-registry/index.js",

src/Umbraco.Web.UI.Client/src/packages/content/content/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { UmbEntityFlag } from '@umbraco-cms/backoffice/entity-flag';
12
import type { UmbPropertyValueData } from '@umbraco-cms/backoffice/property';
23
import type { UmbEntityVariantModel } from '@umbraco-cms/backoffice/variant';
34

@@ -32,8 +33,9 @@ export interface UmbContentDetailModel<VariantModelType extends UmbEntityVariant
3233
unique: string;
3334
entityType: string;
3435
variants: Array<VariantModelType>;
36+
flags: Array<UmbEntityFlag>;
3537
}
3638

3739
export interface UmbContentLikeDetailModel
3840
extends UmbElementDetailModel,
39-
Partial<Pick<UmbContentDetailModel, 'variants'>> {}
41+
Partial<Pick<UmbContentDetailModel, 'variants' | 'flags'>> {}

src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-validation-path-translator.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('UmbValidationPropertyPathTranslationController', () => {
3535
},
3636
],
3737
variants: [],
38+
flags: [],
3839
};
3940

4041
beforeEach(async () => {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type * from './types.js';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
2+
3+
// Omitting the unique because we don't need it for flags and its causes trouble because it can be both null and a string
4+
export interface UmbEntityWithFlags extends Omit<UmbEntityModel, 'unique'> {
5+
flags: Array<UmbEntityFlag>;
6+
}
7+
8+
// Omitting the unique because we don't need it for flags and its causes trouble because it can be both null and a string
9+
export interface UmbEntityWithOptionalFlags extends Omit<UmbEntityModel, 'unique'> {
10+
flags?: UmbEntityWithFlags['flags'];
11+
}
12+
13+
export interface UmbEntityFlag {
14+
alias: string;
15+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { UmbLitElement } from '../../lit-element/lit-element.element.js';
2+
import type { ManifestEntitySign } from '../types.js';
3+
import { customElement, html, nothing, property, repeat, state, css } from '@umbraco-cms/backoffice/external/lit';
4+
import { UmbExtensionsElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api';
5+
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
6+
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
7+
import type { UmbEntityFlag } from '@umbraco-cms/backoffice/entity-flag';
8+
9+
@customElement('umb-entity-sign-bundle')
10+
export class UmbEntitySignBundleElement extends UmbLitElement {
11+
#entityType?: string;
12+
#entityFlagAliases?: Array<string>;
13+
14+
@property({ type: String, attribute: 'entity-type', reflect: false })
15+
get entityType(): string | undefined {
16+
return this.#entityType;
17+
}
18+
19+
set entityType(value: string | undefined) {
20+
if (this.#entityType === value) return;
21+
this.#entityType = value;
22+
this.#gotProperties();
23+
}
24+
25+
@property({ type: Array, attribute: false })
26+
get entityFlags(): Array<UmbEntityFlag> | undefined {
27+
return this.#entityFlagAliases?.map((x) => ({ alias: x }));
28+
}
29+
30+
set entityFlags(value: Array<UmbEntityFlag> | undefined) {
31+
const entityFlagAliases = value?.map((x) => x.alias);
32+
// If they are equal return:
33+
if (this.#entityFlagAliases?.join(',') === entityFlagAliases?.join(',')) return;
34+
this.#entityFlagAliases = entityFlagAliases;
35+
this.#gotProperties();
36+
}
37+
38+
@state()
39+
private _signs?: Array<any>;
40+
41+
@state()
42+
private _labels: Map<string, string> = new Map();
43+
44+
private _open = false;
45+
private _hoverTimer?: number;
46+
47+
#signLabelObservations: Array<UmbObserverController<string>> = [];
48+
49+
constructor() {
50+
super();
51+
this.addEventListener('mouseenter', this.#openTooltip);
52+
this.addEventListener('mouseleave', this.#cancelOpen);
53+
}
54+
55+
#manifestFilter = (manifest: ManifestEntitySign) => {
56+
if (manifest.forEntityTypes && !manifest.forEntityTypes.includes(this.#entityType!)) return false;
57+
if (manifest.forEntityFlags && !manifest.forEntityFlags.some((x) => this.#entityFlagAliases?.includes(x)))
58+
return false;
59+
return true;
60+
};
61+
62+
#gotProperties() {
63+
if (!this.#entityType || !this.#entityFlagAliases) {
64+
this.removeUmbControllerByAlias('extensionsInitializer');
65+
this._signs = [];
66+
return;
67+
}
68+
69+
new UmbExtensionsElementAndApiInitializer(
70+
this,
71+
umbExtensionsRegistry,
72+
'entitySign',
73+
(manifest: ManifestEntitySign) => [{ meta: manifest.meta }],
74+
this.#manifestFilter,
75+
(signs) => {
76+
// Clean up old observers
77+
this.#signLabelObservations.forEach((o) => this.removeUmbController(o));
78+
this.#signLabelObservations = [];
79+
80+
// Setup label observers
81+
signs.forEach((sign) => {
82+
if (sign.api?.label) {
83+
const obs = this.observe(
84+
sign.api.label,
85+
(label) => {
86+
this._labels.set(sign.alias, label);
87+
this.requestUpdate('_labels');
88+
},
89+
'_observeSignLabelOf_' + sign.alias,
90+
);
91+
this.#signLabelObservations.push(obs);
92+
} else if (sign.api?.getLabel) {
93+
this._labels.set(sign.alias, sign.api.getLabel() ?? '');
94+
this.requestUpdate('_labels');
95+
}
96+
});
97+
98+
this._signs = signs;
99+
},
100+
'extensionsInitializer',
101+
);
102+
}
103+
104+
#handleHoverTimer(open: boolean, delay: number) {
105+
if (this._hoverTimer) clearTimeout(this._hoverTimer);
106+
this._hoverTimer = window.setTimeout(() => {
107+
this._open = open;
108+
this.requestUpdate();
109+
this._hoverTimer = undefined;
110+
}, delay);
111+
}
112+
113+
#openTooltip = () => {
114+
if (!this._open) {
115+
this.#handleHoverTimer(true, 240);
116+
}
117+
};
118+
119+
#cancelOpen = () => {
120+
if (this._open) {
121+
this.#handleHoverTimer(false, 360);
122+
} else if (this._hoverTimer) {
123+
clearTimeout(this._hoverTimer);
124+
this._hoverTimer = undefined;
125+
}
126+
};
127+
128+
override render() {
129+
return html`
130+
<slot></slot>
131+
${this.#renderBundle()}
132+
`;
133+
}
134+
#renderBundle() {
135+
if (!this._signs || this._signs.length === 0) return nothing;
136+
137+
const first = this._signs?.[0];
138+
if (!first) return nothing;
139+
return html`<div class="infobox ${this._open ? 'is-open' : ''}" style=${`--count:${this._signs.length}`}>
140+
${this.#renderOptions()}
141+
</div>`;
142+
}
143+
144+
#renderOptions() {
145+
return this._signs
146+
? repeat(
147+
this._signs,
148+
(c) => c.alias,
149+
(c, i) => {
150+
return html`<div class="sign-container ${i > 1 ? 'hide-in-overview' : ''}" style=${`--i:${i}`}>
151+
<span class="badge-icon">${c.component}</span><span class="label">${this._labels.get(c.alias)}</span>
152+
</div>`;
153+
},
154+
)
155+
: nothing;
156+
}
157+
158+
static override styles = [
159+
css`
160+
:host {
161+
anchor-name: --entity-sign;
162+
position: relative;
163+
--offset-h: 12px; /* 22px / 16 */
164+
--row-h: 1.36rem; /* 22px / 16 */
165+
--icon-w: 0.75rem; /* 12px / 16 */
166+
--pad-x: 0.25rem; /* 4px / 16 */
167+
--ease: cubic-bezier(0.1, 0, 0.3, 1);
168+
--ease-bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275);
169+
}
170+
171+
.infobox {
172+
position: absolute;
173+
top: 100%;
174+
margin-top: calc(-12px + var(--offset-h));
175+
left: 100%;
176+
margin-left: -6px;
177+
background-color: transparent;
178+
padding: var(--uui-size-2);
179+
padding-left: var(--uui-size-3);
180+
font-size: 8px;
181+
clip-path: inset(-10px calc(100% - 30px) calc(100% - 10px) -20px);
182+
transition:
183+
background-color 80ms 40ms linear,
184+
clip-path 120ms var(--ease-bounce),
185+
font-size 120ms var(--ease);
186+
/*will-change: clip-path;*/
187+
min-height: fit-content;
188+
}
189+
.infobox::before {
190+
content: '';
191+
position: absolute;
192+
top: 0;
193+
left: 0;
194+
right: 100%;
195+
bottom: 100%;
196+
opacity: 0;
197+
border-radius: 3px;
198+
box-shadow: var(--uui-shadow-depth-2);
199+
display: none;
200+
transition:
201+
right 120ms var(--ease-bounce),
202+
bottom 120ms var(--ease-bounce),
203+
opacity 120ms linear,
204+
display 0 120ms;
205+
}
206+
207+
.infobox > .sign-container {
208+
display: flex;
209+
align-items: start;
210+
gap: 3px;
211+
position: relative;
212+
transform: translate(calc((var(--i) * -5px) - 10px), calc((-1 * var(--i) * var(--row-h)) - var(--offset-h)));
213+
transition:
214+
transform 120ms var(--ease),
215+
visibility 0ms linear 120ms opacity 120ms linear;
216+
z-index: calc(var(--count) - var(--i));
217+
/*will-change: transform;*/
218+
pointer-events: none;
219+
}
220+
.infobox > .sign-container.hide-in-overview {
221+
visibility: hidden;
222+
}
223+
224+
.infobox .sign-container .label {
225+
opacity: 0;
226+
transition: opacity 120ms;
227+
}
228+
229+
/*OPEN STATE -- Prevent the hover state in firefox(until support of the position-anchor)*/
230+
@supports (position-anchor: --any-check) {
231+
.infobox {
232+
position: fixed;
233+
position-anchor: --entity-sign;
234+
top: anchor(bottom);
235+
left: anchor(right);
236+
z-index: 1;
237+
}
238+
.infobox.is-open {
239+
z-index: 10;
240+
background-color: var(--uui-color-surface);
241+
font-size: 12px;
242+
color: var(--uui-color-text);
243+
clip-path: inset(-6px);
244+
--umb-sign-bundle-bg: var(--uui-color-surface);
245+
}
246+
.infobox.is-open::before {
247+
right: 0;
248+
bottom: 0;
249+
opacity: 100;
250+
background-color: var(--uui-color-surface);
251+
display: block;
252+
transition:
253+
right 120ms var(--ease-bounce),
254+
bottom 120ms var(--ease-bounce),
255+
opacity 120ms var(--ease),
256+
display 0 0;
257+
}
258+
.infobox.is-open > .sign-container {
259+
transform: none;
260+
align-items: center;
261+
transition: transform 120ms var(--ease);
262+
visibility: visible;
263+
}
264+
.infobox.is-open .sign-container .label {
265+
opacity: 1;
266+
pointer-events: auto;
267+
}
268+
}
269+
`,
270+
];
271+
}
272+
273+
declare global {
274+
interface HTMLElementTagNameMap {
275+
'umb-entity-sign-bundle': UmbEntitySignBundleElement;
276+
}
277+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './entity-sign-bundle.element.js';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { MetaEntitySign } from './entity-sign.extension.js';
2+
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
3+
import type { Observable } from '@umbraco-cms/backoffice/observable-api';
4+
5+
export interface UmbEntitySignApi extends UmbApi {
6+
/**
7+
* Get the label for this sign
8+
* @returns {string} The label
9+
*/
10+
getLabel?: () => string;
11+
12+
/**
13+
* An observable that provides the label for this sign
14+
* @returns { Observable<string>} A label observable
15+
*/
16+
label?: Observable<string>;
17+
}
18+
19+
export interface UmbEntitySignApiArgs<MetaType extends MetaEntitySign = MetaEntitySign> {
20+
meta: MetaType;
21+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { ManifestEntitySign } from './entity-sign.extension.js';
2+
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
3+
4+
export interface UmbEntitySignElement extends UmbControllerHostElement {
5+
manifest?: ManifestEntitySign;
6+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { UmbEntitySignElement } from './entity-sign-element.interface.js';
2+
import type { UmbEntitySignApi } from './entity-sign-api.interface.js';
3+
import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
4+
5+
/**
6+
* An action to perform on an entity
7+
* For example for content you may wish to create a new document etc
8+
*/
9+
export interface ManifestEntitySign<MetaType extends MetaEntitySign = MetaEntitySign>
10+
extends ManifestElementAndApi<UmbEntitySignElement, UmbEntitySignApi>,
11+
ManifestWithDynamicConditions<UmbExtensionConditionConfig> {
12+
type: 'entitySign';
13+
forEntityTypes?: Array<string>;
14+
forEntityFlags?: Array<string>;
15+
meta: MetaType;
16+
}
17+
18+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
19+
export interface MetaEntitySign {}
20+
21+
declare global {
22+
interface UmbExtensionManifestMap {
23+
umbEntitySign: ManifestEntitySign;
24+
}
25+
}

0 commit comments

Comments
 (0)