Skip to content

Commit e0abc90

Browse files
neo-gpttobiu
andauthored
feat(portal): lazily load content tree children (#12218) (#12279)
* feat(portal): lazily load content tree children (#12218) * fix(portal): opt in tickets tree lazy loading (#12218) * fix(tree): await accordion folder toggles (#12218) --------- Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
1 parent 4367e96 commit e0abc90

7 files changed

Lines changed: 477 additions & 8 deletions

File tree

apps/portal/model/Ticket.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,21 @@ class Ticket extends Model {
1515
* @member {Object[]} fields
1616
*/
1717
fields: [{
18+
name: 'childCount',
19+
type: 'Integer'
20+
}, {
21+
name: 'childrenUrl',
22+
type: 'String'
23+
}, {
1824
name : 'collapsed',
1925
type : 'Boolean',
2026
defaultValue: true
27+
}, {
28+
name: 'contentDir',
29+
type: 'String'
30+
}, {
31+
name: 'filePrefix',
32+
type: 'String'
2133
}, {
2234
name: 'id',
2335
type: 'String'

apps/portal/view/news/tickets/MainContainer.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ class MainContainer extends SharedContainer {
4343
*/
4444
stateProvider: StateProvider,
4545
/**
46-
* @member {Object} treeConfig={displayField:'treeNodeName'}
46+
* @member {Object} treeConfig
4747
*/
4848
treeConfig: {
49-
displayField: 'treeNodeName'
49+
displayField : 'treeNodeName',
50+
lazyChildLoad : true,
51+
lazyChildUrlPrefix : '../../apps/portal/resources/data/'
5052
}
5153
}
5254
}

src/app/content/TreeList.mjs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ class TreeList extends BaseTreeList {
3333
* @member {Object} currentPageRecord=null
3434
*/
3535
currentPageRecord_: null,
36+
/**
37+
* @member {Boolean} lazyChildLoad=false
38+
*/
39+
lazyChildLoad: false,
40+
/**
41+
* Optional URL prefix for relative child chunk URLs.
42+
* @member {String|null} lazyChildUrlPrefix=null
43+
*/
44+
lazyChildUrlPrefix: null,
45+
/**
46+
* @member {String} lazyChildUrlField='childrenUrl'
47+
*/
48+
lazyChildUrlField: 'childrenUrl',
3649
/**
3750
* Optional prefix for the route (e.g. '/learn' or '/releases')
3851
* @member {String|null} routePrefix=null
@@ -90,6 +103,109 @@ class TreeList extends BaseTreeList {
90103
}
91104
}
92105

106+
/**
107+
* @summary Resolves the URL used to fetch a folder node's deferred children.
108+
* @param {Object} record
109+
* @returns {String|null}
110+
*/
111+
getLazyChildUrl(record) {
112+
let me = this,
113+
url = record?.[me.lazyChildUrlField];
114+
115+
if (!url) {
116+
return null
117+
}
118+
119+
if (/^(?:[a-z]+:)?\/\//i.test(url) || url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
120+
return url
121+
}
122+
123+
return (me.lazyChildUrlPrefix || '') + url
124+
}
125+
126+
/**
127+
* @summary Adds content paths to chunk leaves whose compact index payload omits them.
128+
* @param {Object[]} records
129+
* @param {Object} parentRecord
130+
* @returns {Object[]}
131+
*/
132+
normalizeLazyChildRecords(records, parentRecord) {
133+
let {contentDir, filePrefix} = parentRecord;
134+
135+
if (!contentDir) {
136+
return records
137+
}
138+
139+
filePrefix ??= '';
140+
141+
return records.map(record => {
142+
if (!record.path && record.id) {
143+
return {
144+
...record,
145+
path: `${contentDir}/${filePrefix}${record.id}.md`
146+
}
147+
}
148+
149+
return record
150+
})
151+
}
152+
153+
/**
154+
* @summary Loads a folder node's child chunk on first expand when `lazyChildLoad` is enabled.
155+
* @param {Object} record
156+
* @returns {Promise<Boolean|void>} false cancels the folder toggle.
157+
*/
158+
async onFolderItemClick(record) {
159+
let me = this;
160+
161+
if (!me.lazyChildLoad || record.isLeaf || record.isChildrenLoaded) {
162+
return
163+
}
164+
165+
if (me.store.find('parentId', record.id).length > 0) {
166+
record.isChildrenLoaded = true;
167+
return
168+
}
169+
170+
if (record.isLoading) {
171+
return false
172+
}
173+
174+
let url = me.getLazyChildUrl(record);
175+
176+
if (!url) {
177+
return
178+
}
179+
180+
record.isLoading = true;
181+
182+
try {
183+
let response = await fetch(url);
184+
185+
if (!response.ok) {
186+
throw new Error(`Failed to load tree children from ${url}: ${response.status}`)
187+
}
188+
189+
let records = await response.json();
190+
191+
if (!Array.isArray(records)) {
192+
records = records?.data || []
193+
}
194+
195+
if (records.length > 0) {
196+
me.store.add(me.normalizeLazyChildRecords(records, record))
197+
}
198+
199+
record.isChildrenLoaded = true
200+
} catch (error) {
201+
record.hasError = true;
202+
console.error('TreeList lazy child load failed', {error, record, url});
203+
return false
204+
} finally {
205+
record.isLoading = false
206+
}
207+
}
208+
93209
/**
94210
* @param {Object} record
95211
*/
@@ -110,7 +226,11 @@ class TreeList extends BaseTreeList {
110226
onStoreLoad() {
111227
super.onStoreLoad();
112228

113-
this.getStateProvider().data.countPages = this.store.getCount()
229+
let stateProvider = this.getStateProvider();
230+
231+
if (stateProvider) {
232+
stateProvider.data.countPages = this.store.getCount()
233+
}
114234
}
115235
}
116236

src/tree/Accordion.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,12 @@ class AccordionTree extends TreeList {
319319
/**
320320
* @param {Object} item
321321
* @param {Object} data
322+
* @returns {Promise<Boolean|void>}
322323
*/
323-
onItemClick(item, data) {
324-
super.onItemClick(item, data);
324+
async onItemClick(item, data) {
325+
if (await super.onItemClick(item, data) === false) {
326+
return false
327+
}
325328

326329
let me = this,
327330
{selectionModel} = me,
@@ -339,8 +342,6 @@ class AccordionTree extends TreeList {
339342
* @returns {Object} record
340343
*/
341344
me.fire('folderItemClick', {record});
342-
343-
record.collapsed = !record.collapsed
344345
}
345346
}
346347

src/tree/List.mjs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ class Tree extends Base {
575575
* @param {Object} node
576576
* @param {Object} data
577577
*/
578-
onItemClick(node, data) {
578+
async onItemClick(node, data) {
579579
let me = this,
580580
{items} = me.store,
581581
i = 0,
@@ -597,12 +597,28 @@ class Tree extends Base {
597597

598598
if (item) {
599599
if (item.cls?.includes(me.folderCls)) {
600+
if (await me.onFolderItemClick(record, item, data) === false) {
601+
return false
602+
}
603+
604+
item = me.getVdomChild(vnodeId);
605+
606+
if (!item) {
607+
return false
608+
}
609+
600610
NeoArray.toggle(item.cls, 'neo-folder-open');
601611

602612
let isOpen = item.cls.includes('neo-folder-open'),
603613
{parentNode, index} = VDomUtil.find(me.vdom, item.id),
604614
nextSibling = parentNode.cn[index + 1];
605615

616+
if (record.isRecord) {
617+
record.setSilent({collapsed: !isOpen})
618+
} else {
619+
record.collapsed = !isOpen
620+
}
621+
606622
item.style.position = isOpen ? 'sticky' : null;
607623
item.style.top = isOpen ? (item.level * 38) + 'px' : null;
608624

@@ -635,6 +651,18 @@ class Tree extends Base {
635651

636652
}
637653

654+
/**
655+
* @summary Extension hook for subclasses that need to prepare a folder before its
656+
* open state is toggled, e.g. by loading children on demand.
657+
* @param {Object} record
658+
* @param {Object} item
659+
* @param {Object} data
660+
* @returns {Boolean|Promise<Boolean>|void} Return false to cancel the toggle.
661+
*/
662+
onFolderItemClick(record, item, data) {
663+
664+
}
665+
638666
/**
639667
* Gets triggered by clicks on the collapse or expand all icons
640668
* @param {Array} cls

0 commit comments

Comments
 (0)