Skip to content

Commit e0909b0

Browse files
authored
fix(harmonia): the personal (my) surface honors view: range/calendar - a personal calendar page replaces the list (#6352)
A view: range|calendar personal root downgraded to a plain form+list on the My shell - the leave calendar existed only on the power surface. The personal surface now renders its own calendar, exactly like the power one replaces the table: - new my-calendar page/view templates: the user's OWN records as x-h-calendar events (served by the scoped MyController - foreign rows and sensitive fields never arrive); date-click creates with the clicked day prefilled, event-click edits, both through the personal form; - generateUtils: personalCalendarModels (MANAGE_CALENDAR personal roots) + personalListModels (the list pair now excludes calendar roots - replaced, not duplicated); - the shell index routes /my/<Entity> to the personal calendar view and loads its page script. IntentEmissionCoverageIT: a personal view: range fixture (Leave) asserts the personal calendar page reads through the MyController, the view renders x-h-calendar, and /my/<Entity> routes to it. Ran green locally (1/1). Verified live: regenerated the leave module - the My shell renders the month grid with the own request as an event in its start cell (browser-checked).
1 parent 98593a6 commit e0909b0

6 files changed

Lines changed: 209 additions & 4 deletions

File tree

‎components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/my.js‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,31 @@ export const getSources = () => [
1818
action: "generate",
1919
engine: "velocity",
2020
rename: "gen/{{genFolderName}}/js/components/pages/my/{{name}}MyListPage.js",
21-
collection: "personalRootModels"
21+
collection: "personalListModels"
2222
},
2323
{
2424
location: "/template-application-ui-harmonia-java/ui/my/my-list-view.html.template",
2525
action: "generate",
2626
engine: "velocity",
2727
rename: "gen/{{genFolderName}}/views/my/{{name}}-list.html",
28-
collection: "personalRootModels"
28+
collection: "personalListModels"
29+
},
30+
{
31+
// Personal CALENDAR/RANGE layout: a view: calendar|range personal root renders its own
32+
// records as calendar events (scoped to the MyController), replacing the list - exactly
33+
// like the power surface. date-click / event-click route to the personal form.
34+
location: "/template-application-ui-harmonia-java/ui/my/my-calendar-page.js.template",
35+
action: "generate",
36+
engine: "velocity",
37+
rename: "gen/{{genFolderName}}/js/components/pages/my/{{name}}MyCalendarPage.js",
38+
collection: "personalCalendarModels"
39+
},
40+
{
41+
location: "/template-application-ui-harmonia-java/ui/my/my-calendar-view.html.template",
42+
action: "generate",
43+
engine: "velocity",
44+
rename: "gen/{{genFolderName}}/views/my/{{name}}-calendar.html",
45+
collection: "personalCalendarModels"
2946
},
3047
{
3148
location: "/template-application-ui-harmonia-java/ui/my/my-form-page.js.template",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Generated by Eclipse Dirigible based on model and template.
3+
*
4+
* Do not modify the content as it may be re-generated again.
5+
*/
6+
7+
/*
8+
* Personal (my) calendar page for ${name} - the logged-in user's OWN records rendered as events on
9+
* a Harmonia x-h-calendar (the same view: calendar/range surface the power page has, scoped to the
10+
* ${name}MyController - foreign rows and sensitive fields never arrive). date-click / event-click
11+
* route to the personal form.
12+
*/
13+
document.addEventListener('alpine:init', () => {
14+
Alpine.data('${name}MyCalendarPage', () => ({
15+
...basePage(),
16+
state: 'loading',
17+
error: null,
18+
items: [],
19+
// Reactive config for x-h-calendar; repopulated after each load so the calendar re-renders.
20+
calCfg: { view: '${calendarInitialView}', events: [] },
21+
22+
// Controller path for the PERSONAL surface, relative to App.config.restBase.
23+
apiPath: '/${javaPerspectiveName}/${name}MyController',
24+
25+
async init() {
26+
await this.load();
27+
},
28+
29+
async load() {
30+
this.state = 'loading';
31+
this.error = null;
32+
try {
33+
this.items = await App.services.api.get(this.apiPath) || [];
34+
this.calCfg = { view: '${calendarInitialView}', events: this.buildEvents() };
35+
this.state = 'default';
36+
} catch (e) {
37+
this.error = (e && e.message) || 'Could not load your ${menuLabel}.';
38+
this.state = 'error';
39+
}
40+
this.refreshIcons();
41+
},
42+
43+
// Map each record to a calendar event. Rows with no start value are skipped.
44+
buildEvents() {
45+
return (this.items || []).map(row => {
46+
const start = this.toISO(row.${calendarStartProperty});
47+
if (!start) return null;
48+
const ev = {
49+
id: String(row.${primaryKeysString}),
50+
title: this.titleFor(row),
51+
start: start,
52+
allDay: #if($calendarRange)true#{else}this.isDateOnly(row.${calendarStartProperty})#end
53+
};
54+
#if($calendarEndProperty)
55+
const end = this.toISO(row.${calendarEndProperty});
56+
if (end) ev.end = end;
57+
#end
58+
#if($calendarColorProperty)
59+
ev.color = this.colorFor(row.${calendarColorProperty});
60+
#end
61+
return ev;
62+
}).filter(Boolean);
63+
},
64+
65+
titleFor(row) {
66+
#if($calendarTitleProperty)
67+
const t = row.${calendarTitleProperty};
68+
if (t !== undefined && t !== null && String(t) !== '') return String(t);
69+
#end
70+
return '${menuLabel} #' + row.${primaryKeysString};
71+
},
72+
73+
// Jackson serializes java.time as arrays (LocalDate [y,m,d]; LocalDateTime [y,m,d,h,mi,s,ns]) and
74+
// Instant/Timestamp as a numeric epoch. Convert any of those (or a plain ISO string) to the ISO
75+
// string x-h-calendar expects; '' when unset.
76+
toISO(v) {
77+
if (v === undefined || v === null || v === '') return '';
78+
if (Array.isArray(v)) {
79+
const p = n => String(n).padStart(2, '0');
80+
const date = v[0] + '-' + p(v[1]) + '-' + p(v[2]);
81+
if (v.length <= 3) return date;
82+
return date + 'T' + p(v[3] || 0) + ':' + p(v[4] || 0) + ':' + p(v[5] || 0);
83+
}
84+
if (typeof v === 'number') {
85+
const ms = v < 1e12 ? v * 1000 : v;
86+
try { return new Date(ms).toISOString(); } catch (e) { return ''; }
87+
}
88+
return String(v);
89+
},
90+
isDateOnly(v) {
91+
return Array.isArray(v) ? v.length <= 3 : (typeof v === 'string' && v.length <= 10);
92+
},
93+
94+
// Deterministic categorical colour from the Harmonia calendar palette.
95+
colorFor(v) {
96+
const palette = ['blue', 'green', 'purple', 'orange', 'teal', 'pink', 'indigo', 'yellow', 'red', 'gray'];
97+
const key = (v === undefined || v === null) ? '' : String(v);
98+
let h = 0;
99+
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) >>> 0;
100+
return palette[h % palette.length];
101+
},
102+
103+
// Empty-cell click -> create your own record, prefilling the start field from the clicked day
104+
// (the personal form presets any field whose name matches a query param on create).
105+
onDateClick(e) {
106+
const d = e && e.detail ? e.detail.date : null;
107+
let qs = '';
108+
if (d instanceof Date && !isNaN(d.getTime())) {
109+
const p = n => String(n).padStart(2, '0');
110+
let val = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
111+
const time = e.detail.time;
112+
if (time) val += 'T' + time;
113+
qs = '?${calendarStartProperty}=' + encodeURIComponent(val);
114+
}
115+
window.PineconeRouter.navigate('/my/${name}/create' + qs);
116+
},
117+
onEventClick(e) {
118+
const id = e && e.detail && e.detail.event ? e.detail.event.id : null;
119+
if (id) window.PineconeRouter.navigate('/my/${name}/' + encodeURIComponent(id) + '/edit');
120+
},
121+
newEntity() { window.PineconeRouter.navigate('/my/${name}/create'); }
122+
}));
123+
}, { once: true });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!--
2+
Generated by Eclipse Dirigible based on model and template.
3+
Personal (my) calendar fragment for ${name}. Rendered into #app on /my/${name} - the logged-in
4+
user's own records as events; date-click / event-click route to the personal form.
5+
-->
6+
<div x-data="${name}MyCalendarPage" class="vbox size-full">
7+
8+
<div x-h-toolbar data-variant="transparent">
9+
<span x-h-toolbar-title class="shrink-0" x-text="'My ' + T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}')"></span>
10+
<div x-h-toolbar-spacer></div>
11+
<button x-h-button data-variant="primary" @click="newEntity()">
12+
<i role="img" x-h-lucide data-lucide="plus"></i>
13+
<span x-text="T('$projectName:${tprefix}.defaults.new', 'New')"></span>
14+
</button>
15+
</div>
16+
17+
<div x-show="state === 'loading'" class="p-4"><div x-h-spinner></div></div>
18+
<div x-show="state === 'error'" class="p-4" x-h-text.muted x-text="error"></div>
19+
20+
<!-- Calendar (own events only - served by the scoped MyController) -->
21+
<div x-show="state === 'default'" class="bk-stretch" style="flex: 1; min-height: 0; padding: 0.5rem;">
22+
<div x-h-calendar="calCfg" style="height: 100%; min-height: 560px;"
23+
@event-click="onEventClick" @date-click="onDateClick"></div>
24+
</div>
25+
26+
</div>

‎components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/shell/index.html.template‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,10 @@
415415
<!-- PERSONAL (my) surface: the user's own records - a list per root, a form per personal
416416
entity (children are reached from their parent's panels). -->
417417
#foreach($entity in $models)
418-
#if($entity.personalProperty)
418+
#if($entity.personalProperty && $entity.layoutType == "MANAGE_CALENDAR")
419+
<!-- a view: calendar/range personal root lands on its calendar, exactly like the power surface -->
420+
<template x-route="/my/${entity.name}" x-template.target.app="./views/my/${entity.name}-calendar.html"></template>
421+
#elseif($entity.personalProperty)
419422
<template x-route="/my/${entity.name}" x-template.target.app="./views/my/${entity.name}-list.html"></template>
420423
#end
421424
#if($entity.personalProperty && $entity.layoutType == "MANAGE_DOCUMENT")
@@ -524,7 +527,9 @@
524527
#if($entity.type == "REPORT")
525528
<script src="./js/components/pages/${entity.perspectiveName}/${entity.name}ReportPage.js" defer></script>
526529
#end
527-
#if($entity.personalProperty)
530+
#if($entity.personalProperty && $entity.layoutType == "MANAGE_CALENDAR")
531+
<script src="./js/components/pages/my/${entity.name}MyCalendarPage.js" defer></script>
532+
#elseif($entity.personalProperty)
528533
<script src="./js/components/pages/my/${entity.name}MyListPage.js" defer></script>
529534
#end
530535
#if($entity.personalProperty && $entity.layoutType == "MANAGE_DOCUMENT")

‎components/ui/service-generate/src/main/resources/META-INF/dirigible/service-generate/template/generateUtils.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ export function generateFiles(model, parameters, templateSources) {
269269
// Roots only (a DIRECT personal owner): these get list pages + shell perspectives; children
270270
// reach their forms through the parent's panels, never through navigation.
271271
const personalRootModels = model.entities.filter(e => e.personalProperty);
272+
// A personal CALENDAR/RANGE root (view: calendar|range) lands on a personal calendar page
273+
// instead of the list - exactly like the power surface, where the calendar replaces the table.
274+
const personalCalendarModels = model.entities.filter(e => e.layoutType === "MANAGE_CALENDAR" && e.personalProperty);
275+
// The personal LIST pair renders for every root EXCEPT a calendar root (replaced above).
276+
const personalListModels = model.entities.filter(e => e.personalProperty && e.layoutType !== "MANAGE_CALENDAR");
272277
// A personal document root (MANAGE_DOCUMENT + a direct personal owner) gets the personal DOCUMENT
273278
// layout (header form + inline items table + status pill + totals). It still gets a MyController
274279
// (personalModels, above) and a list + perspective (personalRootModels) - only its FORM is the
@@ -367,6 +372,12 @@ export function generateFiles(model, parameters, templateSources) {
367372
case "personalDocumentModels":
368373
generatedFiles.push(...generateCollection(location, content, template, personalDocumentModels, parameters));
369374
break;
375+
case "personalListModels":
376+
generatedFiles.push(...generateCollection(location, content, template, personalListModels, parameters));
377+
break;
378+
case "personalCalendarModels":
379+
generatedFiles.push(...generateCollection(location, content, template, personalCalendarModels, parameters));
380+
break;
370381
case "personalFormModels":
371382
generatedFiles.push(...generateCollection(location, content, template, personalFormModels, parameters));
372383
break;

‎tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,18 @@ class IntentEmissionCoverageIT extends IntegrationTest {
214214
relations:
215215
- { name: Ticket, kind: manyToOne, to: Ticket, composition: true, required: true }
216216
217+
# view: range + a personal owner - the PERSONAL surface must render the range
218+
# calendar (never the plain form+list), scoped to the MyController (U3 parity).
219+
- name: Leave
220+
view: range
221+
calendar: { start: fromDate, end: toDate }
222+
fields:
223+
- { name: id, type: integer, primaryKey: true, generated: true }
224+
- { name: fromDate, type: date, required: true }
225+
- { name: toDate, type: date, required: true }
226+
relations:
227+
- { name: Person, kind: manyToOne, to: Person, personal: true }
228+
217229
# partner: the EXTERNAL-partner mirror of personal - PartnerTicket is owned by a Person
218230
# (reusing identity: email; the admin seed maps the IT user), with a sensitive field.
219231
- name: PartnerTicket
@@ -609,6 +621,17 @@ private void assertEmission() {
609621
assertTrue(myTicketPage.contains("sendMessage") && myTicketPage.contains("TicketMessageMyController"),
610622
"the personal chat composer must append through the personal items controller");
611623

624+
// view: range/calendar + personal - the personal surface renders the calendar (never the
625+
// plain form+list), reads through the scoped controller, and /my/<Entity> lands on it.
626+
String myLeaveCalendar = contentOf("gen/emission/js/components/pages/my/LeaveMyCalendarPage.js");
627+
assertTrue(myLeaveCalendar.contains("LeaveMyController"), "the personal calendar must read through the scoped controller");
628+
String myLeaveView = contentOf("gen/emission/views/my/Leave-calendar.html");
629+
assertTrue(myLeaveView.contains("x-h-calendar"),
630+
"the personal surface of a range/calendar root must render the calendar, not a plain list");
631+
String shellIndex = contentOf("gen/emission/index.html");
632+
assertTrue(shellIndex.contains("x-template.target.app=\"./views/my/Leave-calendar.html\""),
633+
"/my/<Entity> must land on the personal calendar for a calendar root");
634+
612635
// transitions: the server half is a controller that guards the source status + the when
613636
// guard (409) and flips ONLY the status column via the targeted updateProperty; the client
614637
// half is a custom-action contribution carrying the endpoint.

0 commit comments

Comments
 (0)