Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,31 @@ export const getSources = () => [
action: "generate",
engine: "velocity",
rename: "gen/{{genFolderName}}/js/components/pages/my/{{name}}MyListPage.js",
collection: "personalRootModels"
collection: "personalListModels"
},
{
location: "/template-application-ui-harmonia-java/ui/my/my-list-view.html.template",
action: "generate",
engine: "velocity",
rename: "gen/{{genFolderName}}/views/my/{{name}}-list.html",
collection: "personalRootModels"
collection: "personalListModels"
},
{
// Personal CALENDAR/RANGE layout: a view: calendar|range personal root renders its own
// records as calendar events (scoped to the MyController), replacing the list - exactly
// like the power surface. date-click / event-click route to the personal form.
location: "/template-application-ui-harmonia-java/ui/my/my-calendar-page.js.template",
action: "generate",
engine: "velocity",
rename: "gen/{{genFolderName}}/js/components/pages/my/{{name}}MyCalendarPage.js",
collection: "personalCalendarModels"
},
{
location: "/template-application-ui-harmonia-java/ui/my/my-calendar-view.html.template",
action: "generate",
engine: "velocity",
rename: "gen/{{genFolderName}}/views/my/{{name}}-calendar.html",
collection: "personalCalendarModels"
},
{
location: "/template-application-ui-harmonia-java/ui/my/my-form-page.js.template",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Generated by Eclipse Dirigible based on model and template.
*
* Do not modify the content as it may be re-generated again.
*/

/*
* Personal (my) calendar page for ${name} - the logged-in user's OWN records rendered as events on
* a Harmonia x-h-calendar (the same view: calendar/range surface the power page has, scoped to the
* ${name}MyController - foreign rows and sensitive fields never arrive). date-click / event-click
* route to the personal form.
*/
document.addEventListener('alpine:init', () => {
Alpine.data('${name}MyCalendarPage', () => ({
...basePage(),
state: 'loading',
error: null,
items: [],
// Reactive config for x-h-calendar; repopulated after each load so the calendar re-renders.
calCfg: { view: '${calendarInitialView}', events: [] },

// Controller path for the PERSONAL surface, relative to App.config.restBase.
apiPath: '/${javaPerspectiveName}/${name}MyController',

async init() {
await this.load();
},

async load() {
this.state = 'loading';
this.error = null;
try {
this.items = await App.services.api.get(this.apiPath) || [];
this.calCfg = { view: '${calendarInitialView}', events: this.buildEvents() };
this.state = 'default';
} catch (e) {
this.error = (e && e.message) || 'Could not load your ${menuLabel}.';
this.state = 'error';
}
this.refreshIcons();
},

// Map each record to a calendar event. Rows with no start value are skipped.
buildEvents() {
return (this.items || []).map(row => {
const start = this.toISO(row.${calendarStartProperty});
if (!start) return null;
const ev = {
id: String(row.${primaryKeysString}),
title: this.titleFor(row),
start: start,
allDay: #if($calendarRange)true#{else}this.isDateOnly(row.${calendarStartProperty})#end
};
#if($calendarEndProperty)
const end = this.toISO(row.${calendarEndProperty});
if (end) ev.end = end;
#end
#if($calendarColorProperty)
ev.color = this.colorFor(row.${calendarColorProperty});
#end
return ev;
}).filter(Boolean);
},

titleFor(row) {
#if($calendarTitleProperty)
const t = row.${calendarTitleProperty};
if (t !== undefined && t !== null && String(t) !== '') return String(t);
#end
return '${menuLabel} #' + row.${primaryKeysString};
},

// Jackson serializes java.time as arrays (LocalDate [y,m,d]; LocalDateTime [y,m,d,h,mi,s,ns]) and
// Instant/Timestamp as a numeric epoch. Convert any of those (or a plain ISO string) to the ISO
// string x-h-calendar expects; '' when unset.
toISO(v) {
if (v === undefined || v === null || v === '') return '';
if (Array.isArray(v)) {
const p = n => String(n).padStart(2, '0');
const date = v[0] + '-' + p(v[1]) + '-' + p(v[2]);
if (v.length <= 3) return date;
return date + 'T' + p(v[3] || 0) + ':' + p(v[4] || 0) + ':' + p(v[5] || 0);
}
if (typeof v === 'number') {
const ms = v < 1e12 ? v * 1000 : v;
try { return new Date(ms).toISOString(); } catch (e) { return ''; }
}
return String(v);
},
isDateOnly(v) {
return Array.isArray(v) ? v.length <= 3 : (typeof v === 'string' && v.length <= 10);
},

// Deterministic categorical colour from the Harmonia calendar palette.
colorFor(v) {
const palette = ['blue', 'green', 'purple', 'orange', 'teal', 'pink', 'indigo', 'yellow', 'red', 'gray'];
const key = (v === undefined || v === null) ? '' : String(v);
let h = 0;
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) >>> 0;
return palette[h % palette.length];
},

// Empty-cell click -> create your own record, prefilling the start field from the clicked day
// (the personal form presets any field whose name matches a query param on create).
onDateClick(e) {
const d = e && e.detail ? e.detail.date : null;
let qs = '';
if (d instanceof Date && !isNaN(d.getTime())) {
const p = n => String(n).padStart(2, '0');
let val = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
const time = e.detail.time;
if (time) val += 'T' + time;
qs = '?${calendarStartProperty}=' + encodeURIComponent(val);
}
window.PineconeRouter.navigate('/my/${name}/create' + qs);
},
onEventClick(e) {
const id = e && e.detail && e.detail.event ? e.detail.event.id : null;
if (id) window.PineconeRouter.navigate('/my/${name}/' + encodeURIComponent(id) + '/edit');
},
newEntity() { window.PineconeRouter.navigate('/my/${name}/create'); }
}));
}, { once: true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!--
Generated by Eclipse Dirigible based on model and template.
Personal (my) calendar fragment for ${name}. Rendered into #app on /my/${name} - the logged-in
user's own records as events; date-click / event-click route to the personal form.
-->
<div x-data="${name}MyCalendarPage" class="vbox size-full">

<div x-h-toolbar data-variant="transparent">
<span x-h-toolbar-title class="shrink-0" x-text="'My ' + T('$projectName:${tprefix}.t.${dataName}_plural', '${menuLabel}')"></span>
<div x-h-toolbar-spacer></div>
<button x-h-button data-variant="primary" @click="newEntity()">
<i role="img" x-h-lucide data-lucide="plus"></i>
<span x-text="T('$projectName:${tprefix}.defaults.new', 'New')"></span>
</button>
</div>

<div x-show="state === 'loading'" class="p-4"><div x-h-spinner></div></div>
<div x-show="state === 'error'" class="p-4" x-h-text.muted x-text="error"></div>

<!-- Calendar (own events only - served by the scoped MyController) -->
<div x-show="state === 'default'" class="bk-stretch" style="flex: 1; min-height: 0; padding: 0.5rem;">
<div x-h-calendar="calCfg" style="height: 100%; min-height: 560px;"
@event-click="onEventClick" @date-click="onDateClick"></div>
</div>

</div>
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,10 @@
<!-- PERSONAL (my) surface: the user's own records - a list per root, a form per personal
entity (children are reached from their parent's panels). -->
#foreach($entity in $models)
#if($entity.personalProperty)
#if($entity.personalProperty && $entity.layoutType == "MANAGE_CALENDAR")
<!-- a view: calendar/range personal root lands on its calendar, exactly like the power surface -->
<template x-route="/my/${entity.name}" x-template.target.app="./views/my/${entity.name}-calendar.html"></template>
#elseif($entity.personalProperty)
<template x-route="/my/${entity.name}" x-template.target.app="./views/my/${entity.name}-list.html"></template>
#end
#if($entity.personalProperty && $entity.layoutType == "MANAGE_DOCUMENT")
Expand Down Expand Up @@ -524,7 +527,9 @@
#if($entity.type == "REPORT")
<script src="./js/components/pages/${entity.perspectiveName}/${entity.name}ReportPage.js" defer></script>
#end
#if($entity.personalProperty)
#if($entity.personalProperty && $entity.layoutType == "MANAGE_CALENDAR")
<script src="./js/components/pages/my/${entity.name}MyCalendarPage.js" defer></script>
#elseif($entity.personalProperty)
<script src="./js/components/pages/my/${entity.name}MyListPage.js" defer></script>
#end
#if($entity.personalProperty && $entity.layoutType == "MANAGE_DOCUMENT")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ export function generateFiles(model, parameters, templateSources) {
// Roots only (a DIRECT personal owner): these get list pages + shell perspectives; children
// reach their forms through the parent's panels, never through navigation.
const personalRootModels = model.entities.filter(e => e.personalProperty);
// A personal CALENDAR/RANGE root (view: calendar|range) lands on a personal calendar page
// instead of the list - exactly like the power surface, where the calendar replaces the table.
const personalCalendarModels = model.entities.filter(e => e.layoutType === "MANAGE_CALENDAR" && e.personalProperty);
// The personal LIST pair renders for every root EXCEPT a calendar root (replaced above).
const personalListModels = model.entities.filter(e => e.personalProperty && e.layoutType !== "MANAGE_CALENDAR");
// A personal document root (MANAGE_DOCUMENT + a direct personal owner) gets the personal DOCUMENT
// layout (header form + inline items table + status pill + totals). It still gets a MyController
// (personalModels, above) and a list + perspective (personalRootModels) - only its FORM is the
Expand Down Expand Up @@ -367,6 +372,12 @@ export function generateFiles(model, parameters, templateSources) {
case "personalDocumentModels":
generatedFiles.push(...generateCollection(location, content, template, personalDocumentModels, parameters));
break;
case "personalListModels":
generatedFiles.push(...generateCollection(location, content, template, personalListModels, parameters));
break;
case "personalCalendarModels":
generatedFiles.push(...generateCollection(location, content, template, personalCalendarModels, parameters));
break;
case "personalFormModels":
generatedFiles.push(...generateCollection(location, content, template, personalFormModels, parameters));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,18 @@ class IntentEmissionCoverageIT extends IntegrationTest {
relations:
- { name: Ticket, kind: manyToOne, to: Ticket, composition: true, required: true }

# view: range + a personal owner - the PERSONAL surface must render the range
# calendar (never the plain form+list), scoped to the MyController (U3 parity).
- name: Leave
view: range
calendar: { start: fromDate, end: toDate }
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: fromDate, type: date, required: true }
- { name: toDate, type: date, required: true }
relations:
- { name: Person, kind: manyToOne, to: Person, personal: true }

# partner: the EXTERNAL-partner mirror of personal - PartnerTicket is owned by a Person
# (reusing identity: email; the admin seed maps the IT user), with a sensitive field.
- name: PartnerTicket
Expand Down Expand Up @@ -609,6 +621,17 @@ private void assertEmission() {
assertTrue(myTicketPage.contains("sendMessage") && myTicketPage.contains("TicketMessageMyController"),
"the personal chat composer must append through the personal items controller");

// view: range/calendar + personal - the personal surface renders the calendar (never the
// plain form+list), reads through the scoped controller, and /my/<Entity> lands on it.
String myLeaveCalendar = contentOf("gen/emission/js/components/pages/my/LeaveMyCalendarPage.js");
assertTrue(myLeaveCalendar.contains("LeaveMyController"), "the personal calendar must read through the scoped controller");
String myLeaveView = contentOf("gen/emission/views/my/Leave-calendar.html");
assertTrue(myLeaveView.contains("x-h-calendar"),
"the personal surface of a range/calendar root must render the calendar, not a plain list");
String shellIndex = contentOf("gen/emission/index.html");
assertTrue(shellIndex.contains("x-template.target.app=\"./views/my/Leave-calendar.html\""),
"/my/<Entity> must land on the personal calendar for a calendar root");

// transitions: the server half is a controller that guards the source status + the when
// guard (409) and flips ONLY the status column via the targeted updateProperty; the client
// half is a custom-action contribution carrying the endpoint.
Expand Down
Loading