Skip to content

Commit e87c932

Browse files
committed
feat(principal): map CalDAV room metadata properties
Extend the principal model to extract room-seating-capacity, room-type, room-features, room-building-address, and room-building-room-number from CalDAV principal responses. These properties are defined in the CalDAV standard and already served by Nextcloud room backends, but not yet used by the Calendar frontend. Mapping them into the principal model makes them available for any future UI improvement (e.g. a browsable room finder) without changing how principals are fetched. Backward compatible: properties default to null when not provided by the backend. Also derives roomBuildingName from the building address (first segment) and constructs a roomAddress string suitable for the event LOCATION field. Signed-off-by: Rik Dekker <rik@rikdekker.nl>
1 parent e05a172 commit e87c932

2 files changed

Lines changed: 177 additions & 0 deletions

File tree

src/models/principal.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ function getDefaultPrincipalObject(props) {
5050
principalId: null,
5151
// The url of the default calendar for invitations
5252
scheduleDefaultCalendarUrl: null,
53+
// Room-specific properties (only for calendar-rooms)
54+
roomSeatingCapacity: null,
55+
roomType: null,
56+
roomAddress: null,
57+
roomFeatures: null,
58+
roomBuildingName: null,
59+
roomBuildingAddress: null,
60+
roomNumber: null,
5361
...props,
5462
}
5563
}
@@ -91,6 +99,38 @@ function mapDavToPrincipal(dav) {
9199
const url = dav.principalUrl
92100
const userId = dav.userId
93101

102+
// Extract room-specific properties from DAV object, trimming string values defensively
103+
const roomSeatingCapacity = dav.roomSeatingCapacity ?? null
104+
const roomType = (dav.roomType ?? '').toString().trim() || null
105+
const roomFeatures = (dav.roomFeatures ?? '').toString().trim() || null
106+
// Strip leading/trailing whitespace and commas from building address to handle empty
107+
// building-name fields, e.g. ", Science Park 140, 1098 XG, Amsterdam" → "Science Park 140, 1098 XG, Amsterdam"
108+
const rawBuildingAddress = dav.roomBuildingAddress ?? null
109+
const roomBuildingAddress = rawBuildingAddress
110+
? rawBuildingAddress.replace(/^[\s,]+|[\s,]+$/g, '').trim() || null
111+
: null
112+
// Derive building name from address (everything before first comma): "Poppodium, Kerkstraat 10" → "Poppodium"
113+
const roomBuildingName = roomBuildingAddress ? roomBuildingAddress.split(',')[0].trim() || null : null
114+
// Room number (floor.room format, e.g. "2.17") is stored in room-building-room-number
115+
const roomNumber = (dav.roomBuildingRoomNumber ?? '').toString().trim() || null
116+
117+
// Construct roomAddress for event LOCATION field from available properties
118+
// Format: "Street (Building, Room X.XX)" — street-first for map/navigation apps
119+
let roomAddress = null
120+
if (roomBuildingAddress) {
121+
const commaIdx = roomBuildingAddress.indexOf(',')
122+
if (commaIdx > 0) {
123+
const building = roomBuildingAddress.substring(0, commaIdx).trim()
124+
const street = roomBuildingAddress.substring(commaIdx + 1).trim()
125+
const detail = roomNumber ? building + ', Room ' + roomNumber : building
126+
roomAddress = street + ' (' + detail + ')'
127+
} else {
128+
roomAddress = roomNumber
129+
? roomBuildingAddress + ' (Room ' + roomNumber + ')'
130+
: roomBuildingAddress
131+
}
132+
}
133+
94134
return getDefaultPrincipalObject({
95135
id,
96136
calendarUserType,
@@ -107,6 +147,13 @@ function mapDavToPrincipal(dav) {
107147
principalId,
108148
userId,
109149
scheduleDefaultCalendarUrl,
150+
roomSeatingCapacity,
151+
roomType,
152+
roomAddress,
153+
roomFeatures,
154+
roomBuildingName,
155+
roomBuildingAddress,
156+
roomNumber,
110157
})
111158
}
112159

tests/javascript/unit/models/principal.test.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ describe('Test suite: Principal model (models/principal.js)', () => {
2424
isCalendarRoom: false,
2525
principalId: null,
2626
scheduleDefaultCalendarUrl: null,
27+
roomSeatingCapacity: null,
28+
roomType: null,
29+
roomAddress: null,
30+
roomFeatures: null,
31+
roomBuildingName: null,
32+
roomBuildingAddress: null,
33+
roomNumber: null,
2734
})
2835
})
2936

@@ -48,6 +55,13 @@ describe('Test suite: Principal model (models/principal.js)', () => {
4855
principalId: 'bar',
4956
otherProp: 'foo',
5057
scheduleDefaultCalendarUrl: null,
58+
roomSeatingCapacity: null,
59+
roomType: null,
60+
roomAddress: null,
61+
roomFeatures: null,
62+
roomBuildingName: null,
63+
roomBuildingAddress: null,
64+
roomNumber: null,
5165
})
5266
})
5367

@@ -82,6 +96,13 @@ describe('Test suite: Principal model (models/principal.js)', () => {
8296
isCalendarRoom: false,
8397
principalId: 'jane.doe',
8498
userId: 'legacy-jane-doe-uid',
99+
roomSeatingCapacity: null,
100+
roomType: null,
101+
roomAddress: null,
102+
roomFeatures: null,
103+
roomBuildingName: null,
104+
roomBuildingAddress: null,
105+
roomNumber: null,
85106
})
86107
})
87108

@@ -116,6 +137,13 @@ describe('Test suite: Principal model (models/principal.js)', () => {
116137
isCalendarRoom: false,
117138
principalId: 'jane.doe',
118139
userId: null,
140+
roomSeatingCapacity: null,
141+
roomType: null,
142+
roomAddress: null,
143+
roomFeatures: null,
144+
roomBuildingName: null,
145+
roomBuildingAddress: null,
146+
roomNumber: null,
119147
})
120148
})
121149

@@ -150,6 +178,13 @@ describe('Test suite: Principal model (models/principal.js)', () => {
150178
isCalendarRoom: false,
151179
principalId: 'CGAH82BAS285H',
152180
userId: null,
181+
roomSeatingCapacity: null,
182+
roomType: null,
183+
roomAddress: null,
184+
roomFeatures: null,
185+
roomBuildingName: null,
186+
roomBuildingAddress: null,
187+
roomNumber: null,
153188
})
154189
})
155190

@@ -184,6 +219,13 @@ describe('Test suite: Principal model (models/principal.js)', () => {
184219
isCalendarRoom: false,
185220
principalId: 'projector-123',
186221
userId: null,
222+
roomSeatingCapacity: null,
223+
roomType: null,
224+
roomAddress: null,
225+
roomFeatures: null,
226+
roomBuildingName: null,
227+
roomBuildingAddress: null,
228+
roomNumber: null,
187229
})
188230
})
189231

@@ -218,9 +260,90 @@ describe('Test suite: Principal model (models/principal.js)', () => {
218260
isCalendarRoom: true,
219261
principalId: 'room-123',
220262
userId: null,
263+
roomSeatingCapacity: null,
264+
roomType: null,
265+
roomAddress: null,
266+
roomFeatures: null,
267+
roomBuildingName: null,
268+
roomBuildingAddress: null,
269+
roomNumber: null,
221270
})
222271
})
223272

273+
it('should properly map a calendar-room-principal with room properties', () => {
274+
const dav = {
275+
addressBookHomes: undefined,
276+
calendarHomes: [],
277+
calendarUserAddressSet: [],
278+
calendarUserType: 'ROOM',
279+
displayname: 'Conference Room A',
280+
email: 'conf-a@example.com',
281+
principalScheme: 'principal:principals/calendar-rooms/conf-a',
282+
principalUrl: '/remote.php/dav/principals/calendar-rooms/conf-a/',
283+
scheduleInbox: null,
284+
scheduleOutbox: null,
285+
url: '/remote.php/dav/principals/calendar-rooms/conf-a/',
286+
userId: null,
287+
roomSeatingCapacity: 20,
288+
roomType: 'conference-room',
289+
roomFeatures: 'PROJECTOR,WHITEBOARD',
290+
roomBuildingAddress: 'Building A, Main Street 1',
291+
roomBuildingRoomNumber: '2.17',
292+
}
293+
294+
expect(mapDavToPrincipal(dav)).toEqual({
295+
id: 'L3JlbW90ZS5waHAvZGF2L3ByaW5jaXBhbHMvY2FsZW5kYXItcm9vbXMvY29uZi1hLw==',
296+
dav,
297+
calendarUserType: 'ROOM',
298+
principalScheme: 'principal:principals/calendar-rooms/conf-a',
299+
emailAddress: 'conf-a@example.com',
300+
displayname: 'Conference Room A',
301+
url: '/remote.php/dav/principals/calendar-rooms/conf-a/',
302+
isUser: false,
303+
isGroup: false,
304+
isCircle: false,
305+
isCalendarResource: false,
306+
isCalendarRoom: true,
307+
principalId: 'conf-a',
308+
userId: null,
309+
roomSeatingCapacity: 20,
310+
roomType: 'conference-room',
311+
roomFeatures: 'PROJECTOR,WHITEBOARD',
312+
roomBuildingName: 'Building A',
313+
roomBuildingAddress: 'Building A, Main Street 1',
314+
roomNumber: '2.17',
315+
roomAddress: 'Main Street 1 (Building A, Room 2.17)',
316+
})
317+
})
318+
319+
it('should strip leading commas and whitespace from roomBuildingAddress', () => {
320+
const dav = {
321+
addressBookHomes: undefined,
322+
calendarHomes: [],
323+
calendarUserAddressSet: [],
324+
calendarUserType: 'ROOM',
325+
displayname: 'AMS 0.11',
326+
email: 'ams-011@example.com',
327+
principalScheme: 'principal:principals/calendar-rooms/ams-011',
328+
principalUrl: '/remote.php/dav/principals/calendar-rooms/ams-011/',
329+
scheduleInbox: null,
330+
scheduleOutbox: null,
331+
url: '/remote.php/dav/principals/calendar-rooms/ams-011/',
332+
userId: null,
333+
roomSeatingCapacity: 1,
334+
roomType: 'meeting-room',
335+
roomFeatures: ' ',
336+
roomBuildingAddress: ', Science Park 140, 1098 XG, Amsterdam',
337+
roomBuildingRoomNumber: '0.11',
338+
}
339+
340+
const result = mapDavToPrincipal(dav)
341+
expect(result.roomBuildingAddress).toBe('Science Park 140, 1098 XG, Amsterdam')
342+
expect(result.roomBuildingName).toBe('Science Park 140')
343+
expect(result.roomFeatures).toBe(null)
344+
expect(result.roomAddress).toBe('1098 XG, Amsterdam (Science Park 140, Room 0.11)')
345+
})
346+
224347
it('should properly map a principal from an unknown backend to principal-object', () => {
225348
const dav = {
226349
addressBookHomes: undefined,
@@ -252,6 +375,13 @@ describe('Test suite: Principal model (models/principal.js)', () => {
252375
isCalendarRoom: false,
253376
principalId: null,
254377
userId: null,
378+
roomSeatingCapacity: null,
379+
roomType: null,
380+
roomAddress: null,
381+
roomFeatures: null,
382+
roomBuildingName: null,
383+
roomBuildingAddress: null,
384+
roomNumber: null,
255385
})
256386
})
257387
})

0 commit comments

Comments
 (0)