@xiboplayer/utils 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cms-api.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * CMS API Client — OAuth2-authenticated REST client for Xibo CMS
3
3
  *
4
4
  * Full CRUD client for all Xibo CMS REST API entities: displays, layouts,
5
- * regions, widgets, media, campaigns, schedules, display groups, resolutions.
5
+ * regions, widgets, media, campaigns, schedules, display groups, resolutions,
6
+ * commands, dayparts, playlists, datasets, notifications, folders, and tags.
6
7
  * Implements OAuth2 client_credentials flow (machine-to-machine).
7
8
  *
8
9
  * Usage:
@@ -747,6 +748,805 @@ export class CmsApiClient {
747
748
  async editLayout(layoutId, params) {
748
749
  return this.request('PUT', `/layout/${layoutId}`, params);
749
750
  }
751
+
752
+ // ── Layout Copy / Discard (#25) ────────────────────────────────────
753
+
754
+ /**
755
+ * Copy a layout
756
+ * @param {number} layoutId
757
+ * @param {Object} [opts] - Options (name, description, copyMediaFiles)
758
+ * @returns {Promise<Object>} Copied layout
759
+ */
760
+ async copyLayout(layoutId, opts = {}) {
761
+ return this.post(`/layout/copy/${layoutId}`, opts);
762
+ }
763
+
764
+ /**
765
+ * Discard a draft layout (revert to last published version)
766
+ * @param {number} layoutId
767
+ * @returns {Promise<void>}
768
+ */
769
+ async discardLayout(layoutId) {
770
+ await this.put(`/layout/discard/${layoutId}`);
771
+ }
772
+
773
+ // ── Campaign Edit / Unassign (#26) ─────────────────────────────────
774
+
775
+ /**
776
+ * Edit campaign properties
777
+ * @param {number} campaignId
778
+ * @param {Object} params - Properties to update (name, etc.)
779
+ * @returns {Promise<Object>} Updated campaign
780
+ */
781
+ async editCampaign(campaignId, params) {
782
+ return this.put(`/campaign/${campaignId}`, params);
783
+ }
784
+
785
+ /**
786
+ * Get a single campaign by ID
787
+ * @param {number} campaignId
788
+ * @returns {Promise<Object>}
789
+ */
790
+ async getCampaign(campaignId) {
791
+ return this.get(`/campaign/${campaignId}`);
792
+ }
793
+
794
+ /**
795
+ * Unassign a layout from a campaign
796
+ * @param {number} campaignId
797
+ * @param {number} layoutId
798
+ * @returns {Promise<void>}
799
+ */
800
+ async unassignLayoutFromCampaign(campaignId, layoutId) {
801
+ await this.post(`/campaign/layout/unassign/${campaignId}`, { layoutId });
802
+ }
803
+
804
+ // ── Schedule Edit (#27) ────────────────────────────────────────────
805
+
806
+ /**
807
+ * Edit a schedule event
808
+ * @param {number} eventId
809
+ * @param {Object} params - Properties to update (fromDt, toDt, isPriority, etc.)
810
+ * @returns {Promise<Object>} Updated schedule event
811
+ */
812
+ async editSchedule(eventId, params) {
813
+ return this.put(`/schedule/${eventId}`, params);
814
+ }
815
+
816
+ // ── Layout Retire / Status / Tag (#34) ─────────────────────────────
817
+
818
+ /**
819
+ * Retire a layout (hide from new scheduling but keep existing schedules)
820
+ * @param {number} layoutId
821
+ * @returns {Promise<void>}
822
+ */
823
+ async retireLayout(layoutId) {
824
+ await this.put(`/layout/retire/${layoutId}`);
825
+ }
826
+
827
+ /**
828
+ * Unretire a previously retired layout
829
+ * @param {number} layoutId
830
+ * @returns {Promise<void>}
831
+ */
832
+ async unretireLayout(layoutId) {
833
+ await this.put(`/layout/unretire/${layoutId}`);
834
+ }
835
+
836
+ /**
837
+ * Get layout status (build status, validity)
838
+ * @param {number} layoutId
839
+ * @returns {Promise<Object>}
840
+ */
841
+ async getLayoutStatus(layoutId) {
842
+ return this.get(`/layout/status/${layoutId}`);
843
+ }
844
+
845
+ /**
846
+ * Tag a layout
847
+ * @param {number} layoutId
848
+ * @param {string[]} tags - Tag names to add
849
+ * @returns {Promise<void>}
850
+ */
851
+ async tagLayout(layoutId, tags) {
852
+ await this.post(`/layout/${layoutId}/tag`, { tag: tags.join(',') });
853
+ }
854
+
855
+ /**
856
+ * Remove tags from a layout
857
+ * @param {number} layoutId
858
+ * @param {string[]} tags - Tag names to remove
859
+ * @returns {Promise<void>}
860
+ */
861
+ async untagLayout(layoutId, tags) {
862
+ await this.post(`/layout/${layoutId}/untag`, { tag: tags.join(',') });
863
+ }
864
+
865
+ // ── Command CRUD (#36) ─────────────────────────────────────────────
866
+
867
+ /**
868
+ * List commands
869
+ * @param {Object} [filters] - Filters (commandId, command)
870
+ * @returns {Promise<Array>}
871
+ */
872
+ async listCommands(filters = {}) {
873
+ const data = await this.get('/command', filters);
874
+ return Array.isArray(data) ? data : [];
875
+ }
876
+
877
+ /**
878
+ * Create a command
879
+ * @param {Object} params - { command, description, code }
880
+ * @returns {Promise<Object>}
881
+ */
882
+ async createCommand(params) {
883
+ return this.post('/command', params);
884
+ }
885
+
886
+ /**
887
+ * Edit a command
888
+ * @param {number} commandId
889
+ * @param {Object} params - Properties to update
890
+ * @returns {Promise<Object>}
891
+ */
892
+ async editCommand(commandId, params) {
893
+ return this.put(`/command/${commandId}`, params);
894
+ }
895
+
896
+ /**
897
+ * Delete a command
898
+ * @param {number} commandId
899
+ * @returns {Promise<void>}
900
+ */
901
+ async deleteCommand(commandId) {
902
+ await this.del(`/command/${commandId}`);
903
+ }
904
+
905
+ // ── Display Extras (#41) ───────────────────────────────────────────
906
+
907
+ /**
908
+ * Delete a display
909
+ * @param {number} displayId
910
+ * @returns {Promise<void>}
911
+ */
912
+ async deleteDisplay(displayId) {
913
+ await this.del(`/display/${displayId}`);
914
+ }
915
+
916
+ /**
917
+ * Send Wake-on-LAN to a display
918
+ * @param {number} displayId
919
+ * @returns {Promise<void>}
920
+ */
921
+ async wolDisplay(displayId) {
922
+ await this.post(`/display/wol/${displayId}`);
923
+ }
924
+
925
+ /**
926
+ * Set the default layout for a display
927
+ * @param {number} displayId
928
+ * @param {number} layoutId - Layout to set as default
929
+ * @returns {Promise<Object>}
930
+ */
931
+ async setDefaultLayout(displayId, layoutId) {
932
+ return this.put(`/display/${displayId}`, { defaultLayoutId: layoutId });
933
+ }
934
+
935
+ /**
936
+ * Purge all content from a display (force re-download)
937
+ * @param {number} displayId
938
+ * @returns {Promise<void>}
939
+ */
940
+ async purgeDisplay(displayId) {
941
+ await this.post(`/display/purge/${displayId}`);
942
+ }
943
+
944
+ // ── DayPart CRUD (#24) ─────────────────────────────────────────────
945
+
946
+ /**
947
+ * List day parts
948
+ * @param {Object} [filters] - Filters (dayPartId, name)
949
+ * @returns {Promise<Array>}
950
+ */
951
+ async listDayParts(filters = {}) {
952
+ const data = await this.get('/daypart', filters);
953
+ return Array.isArray(data) ? data : [];
954
+ }
955
+
956
+ /**
957
+ * Create a day part
958
+ * @param {Object} params - { name, description, startTime, endTime, exceptionDays, ... }
959
+ * @returns {Promise<Object>}
960
+ */
961
+ async createDayPart(params) {
962
+ return this.post('/daypart', params);
963
+ }
964
+
965
+ /**
966
+ * Edit a day part
967
+ * @param {number} dayPartId
968
+ * @param {Object} params - Properties to update
969
+ * @returns {Promise<Object>}
970
+ */
971
+ async editDayPart(dayPartId, params) {
972
+ return this.put(`/daypart/${dayPartId}`, params);
973
+ }
974
+
975
+ /**
976
+ * Delete a day part
977
+ * @param {number} dayPartId
978
+ * @returns {Promise<void>}
979
+ */
980
+ async deleteDayPart(dayPartId) {
981
+ await this.del(`/daypart/${dayPartId}`);
982
+ }
983
+
984
+ // ── Library Extensions (#33) ───────────────────────────────────────
985
+
986
+ /**
987
+ * Upload media from a URL
988
+ * @param {string} url - Remote URL to download
989
+ * @param {string} name - Name for the media item
990
+ * @returns {Promise<Object>}
991
+ */
992
+ async uploadMediaUrl(url, name) {
993
+ return this.post('/library', { url, name, type: 'uri' });
994
+ }
995
+
996
+ /**
997
+ * Copy a media item
998
+ * @param {number} mediaId
999
+ * @returns {Promise<Object>} Copied media
1000
+ */
1001
+ async copyMedia(mediaId) {
1002
+ return this.post(`/library/copy/${mediaId}`);
1003
+ }
1004
+
1005
+ /**
1006
+ * Download a media file (returns raw response for streaming)
1007
+ * @param {number} mediaId
1008
+ * @returns {Promise<Response>} Raw fetch response
1009
+ */
1010
+ async downloadMedia(mediaId) {
1011
+ await this.ensureToken();
1012
+ const url = `${this.baseUrl}/api/library/download/${mediaId}`;
1013
+ const response = await fetch(url, {
1014
+ headers: { 'Authorization': `Bearer ${this.accessToken}` }
1015
+ });
1016
+ if (!response.ok) {
1017
+ const text = await response.text();
1018
+ throw new CmsApiError('GET', `/library/download/${mediaId}`, response.status, text);
1019
+ }
1020
+ return response;
1021
+ }
1022
+
1023
+ /**
1024
+ * Edit media properties
1025
+ * @param {number} mediaId
1026
+ * @param {Object} params - Properties to update (name, duration, retired, etc.)
1027
+ * @returns {Promise<Object>}
1028
+ */
1029
+ async editMedia(mediaId, params) {
1030
+ return this.put(`/library/${mediaId}`, params);
1031
+ }
1032
+
1033
+ /**
1034
+ * Get media usage (which layouts/playlists reference this media)
1035
+ * @param {number} mediaId
1036
+ * @returns {Promise<Object>}
1037
+ */
1038
+ async getMediaUsage(mediaId) {
1039
+ return this.get(`/library/usage/${mediaId}`);
1040
+ }
1041
+
1042
+ /**
1043
+ * Tidy the library (remove unused/old revisions)
1044
+ * @returns {Promise<void>}
1045
+ */
1046
+ async tidyLibrary() {
1047
+ await this.post('/library/tidy');
1048
+ }
1049
+
1050
+ // ── Playlist CRUD (#35) ────────────────────────────────────────────
1051
+
1052
+ /**
1053
+ * List playlists
1054
+ * @param {Object} [filters] - Filters (playlistId, name, etc.)
1055
+ * @returns {Promise<Array>}
1056
+ */
1057
+ async listPlaylists(filters = {}) {
1058
+ const data = await this.get('/playlist', filters);
1059
+ return Array.isArray(data) ? data : [];
1060
+ }
1061
+
1062
+ /**
1063
+ * Create a playlist
1064
+ * @param {string} name
1065
+ * @returns {Promise<Object>}
1066
+ */
1067
+ async createPlaylist(name) {
1068
+ return this.post('/playlist', { name });
1069
+ }
1070
+
1071
+ /**
1072
+ * Get a single playlist by ID
1073
+ * @param {number} playlistId
1074
+ * @returns {Promise<Object>}
1075
+ */
1076
+ async getPlaylist(playlistId) {
1077
+ return this.get(`/playlist/${playlistId}`);
1078
+ }
1079
+
1080
+ /**
1081
+ * Edit playlist properties
1082
+ * @param {number} playlistId
1083
+ * @param {Object} params - Properties to update
1084
+ * @returns {Promise<Object>}
1085
+ */
1086
+ async editPlaylist(playlistId, params) {
1087
+ return this.put(`/playlist/${playlistId}`, params);
1088
+ }
1089
+
1090
+ /**
1091
+ * Delete a playlist
1092
+ * @param {number} playlistId
1093
+ * @returns {Promise<void>}
1094
+ */
1095
+ async deletePlaylist(playlistId) {
1096
+ await this.del(`/playlist/${playlistId}`);
1097
+ }
1098
+
1099
+ /**
1100
+ * Reorder widgets in a playlist
1101
+ * @param {number} playlistId
1102
+ * @param {number[]} widgetIds - Ordered widget IDs
1103
+ * @returns {Promise<void>}
1104
+ */
1105
+ async reorderPlaylist(playlistId, widgetIds) {
1106
+ await this.ensureToken();
1107
+ const url = `${this.baseUrl}/api/playlist/order/${playlistId}`;
1108
+ const urlParams = new URLSearchParams();
1109
+ for (const id of widgetIds) {
1110
+ urlParams.append('widgets[]', String(id));
1111
+ }
1112
+ const response = await fetch(url, {
1113
+ method: 'POST',
1114
+ headers: {
1115
+ 'Authorization': `Bearer ${this.accessToken}`,
1116
+ 'Content-Type': 'application/x-www-form-urlencoded'
1117
+ },
1118
+ body: urlParams
1119
+ });
1120
+ if (!response.ok) {
1121
+ const text = await response.text();
1122
+ throw new CmsApiError('POST', `/playlist/order/${playlistId}`, response.status, text);
1123
+ }
1124
+ }
1125
+
1126
+ /**
1127
+ * Copy a playlist
1128
+ * @param {number} playlistId
1129
+ * @returns {Promise<Object>} Copied playlist
1130
+ */
1131
+ async copyPlaylist(playlistId) {
1132
+ return this.post(`/playlist/copy/${playlistId}`);
1133
+ }
1134
+
1135
+ // ── Widget Extras (#37) ────────────────────────────────────────────
1136
+
1137
+ /**
1138
+ * Set transition for a widget
1139
+ * @param {number} widgetId
1140
+ * @param {string} type - Transition type (e.g. 'fly', 'fade')
1141
+ * @param {Object} [config] - Transition config (duration, direction)
1142
+ * @returns {Promise<Object>}
1143
+ */
1144
+ async setWidgetTransition(widgetId, type, config = {}) {
1145
+ return this.put(`/playlist/widget/transition/${widgetId}`, { type, ...config });
1146
+ }
1147
+
1148
+ /**
1149
+ * Set audio for a widget
1150
+ * @param {number} widgetId
1151
+ * @param {Object} params - { mediaId, volume, loop }
1152
+ * @returns {Promise<Object>}
1153
+ */
1154
+ async setWidgetAudio(widgetId, params) {
1155
+ return this.put(`/playlist/widget/${widgetId}/audio`, params);
1156
+ }
1157
+
1158
+ /**
1159
+ * Remove audio from a widget
1160
+ * @param {number} widgetId
1161
+ * @returns {Promise<void>}
1162
+ */
1163
+ async removeWidgetAudio(widgetId) {
1164
+ await this.del(`/playlist/widget/${widgetId}/audio`);
1165
+ }
1166
+
1167
+ /**
1168
+ * Set expiry dates for a widget
1169
+ * @param {number} widgetId
1170
+ * @param {Object} params - { fromDt, toDt, deleteOnExpiry }
1171
+ * @returns {Promise<Object>}
1172
+ */
1173
+ async setWidgetExpiry(widgetId, params) {
1174
+ return this.put(`/playlist/widget/${widgetId}/expiry`, params);
1175
+ }
1176
+
1177
+ // ── Template Save / Manage (#39) ───────────────────────────────────
1178
+
1179
+ /**
1180
+ * Save a layout as a template
1181
+ * @param {number} layoutId
1182
+ * @param {Object} params - { name, description, includeWidgets }
1183
+ * @returns {Promise<Object>}
1184
+ */
1185
+ async saveAsTemplate(layoutId, params) {
1186
+ return this.post(`/template/${layoutId}`, params);
1187
+ }
1188
+
1189
+ /**
1190
+ * Get a single template by ID
1191
+ * @param {number} templateId
1192
+ * @returns {Promise<Object>}
1193
+ */
1194
+ async getTemplate(templateId) {
1195
+ return this.get(`/template/${templateId}`);
1196
+ }
1197
+
1198
+ /**
1199
+ * Delete a template
1200
+ * @param {number} templateId
1201
+ * @returns {Promise<void>}
1202
+ */
1203
+ async deleteTemplate(templateId) {
1204
+ await this.del(`/template/${templateId}`);
1205
+ }
1206
+
1207
+ // ── Dataset CRUD (#28) ─────────────────────────────────────────────
1208
+
1209
+ /**
1210
+ * List datasets
1211
+ * @param {Object} [filters] - Filters (dataSetId, dataSet)
1212
+ * @returns {Promise<Array>}
1213
+ */
1214
+ async listDatasets(filters = {}) {
1215
+ const data = await this.get('/dataset', filters);
1216
+ return Array.isArray(data) ? data : [];
1217
+ }
1218
+
1219
+ /**
1220
+ * Create a dataset
1221
+ * @param {Object} params - { dataSet, description }
1222
+ * @returns {Promise<Object>}
1223
+ */
1224
+ async createDataset(params) {
1225
+ return this.post('/dataset', params);
1226
+ }
1227
+
1228
+ /**
1229
+ * Edit a dataset
1230
+ * @param {number} dataSetId
1231
+ * @param {Object} params
1232
+ * @returns {Promise<Object>}
1233
+ */
1234
+ async editDataset(dataSetId, params) {
1235
+ return this.put(`/dataset/${dataSetId}`, params);
1236
+ }
1237
+
1238
+ /**
1239
+ * Delete a dataset
1240
+ * @param {number} dataSetId
1241
+ * @returns {Promise<void>}
1242
+ */
1243
+ async deleteDataset(dataSetId) {
1244
+ await this.del(`/dataset/${dataSetId}`);
1245
+ }
1246
+
1247
+ /**
1248
+ * List columns in a dataset
1249
+ * @param {number} dataSetId
1250
+ * @returns {Promise<Array>}
1251
+ */
1252
+ async listDatasetColumns(dataSetId) {
1253
+ const data = await this.get(`/dataset/${dataSetId}/column`);
1254
+ return Array.isArray(data) ? data : [];
1255
+ }
1256
+
1257
+ /**
1258
+ * Create a column in a dataset
1259
+ * @param {number} dataSetId
1260
+ * @param {Object} params - { heading, dataTypeId, dataSetColumnTypeId, listContent, ... }
1261
+ * @returns {Promise<Object>}
1262
+ */
1263
+ async createDatasetColumn(dataSetId, params) {
1264
+ return this.post(`/dataset/${dataSetId}/column`, params);
1265
+ }
1266
+
1267
+ /**
1268
+ * Edit a dataset column
1269
+ * @param {number} dataSetId
1270
+ * @param {number} columnId
1271
+ * @param {Object} params
1272
+ * @returns {Promise<Object>}
1273
+ */
1274
+ async editDatasetColumn(dataSetId, columnId, params) {
1275
+ return this.put(`/dataset/${dataSetId}/column/${columnId}`, params);
1276
+ }
1277
+
1278
+ /**
1279
+ * Delete a dataset column
1280
+ * @param {number} dataSetId
1281
+ * @param {number} columnId
1282
+ * @returns {Promise<void>}
1283
+ */
1284
+ async deleteDatasetColumn(dataSetId, columnId) {
1285
+ await this.del(`/dataset/${dataSetId}/column/${columnId}`);
1286
+ }
1287
+
1288
+ /**
1289
+ * List rows (data) in a dataset
1290
+ * @param {number} dataSetId
1291
+ * @param {Object} [filters] - Filters (page, length)
1292
+ * @returns {Promise<Array>}
1293
+ */
1294
+ async listDatasetData(dataSetId, filters = {}) {
1295
+ const data = await this.get(`/dataset/data/${dataSetId}`, filters);
1296
+ return Array.isArray(data) ? data : [];
1297
+ }
1298
+
1299
+ /**
1300
+ * Add a row to a dataset
1301
+ * @param {number} dataSetId
1302
+ * @param {Object} rowData - Column heading → value pairs
1303
+ * @returns {Promise<Object>}
1304
+ */
1305
+ async addDatasetRow(dataSetId, rowData) {
1306
+ return this.post(`/dataset/data/${dataSetId}`, rowData);
1307
+ }
1308
+
1309
+ /**
1310
+ * Edit a dataset row
1311
+ * @param {number} dataSetId
1312
+ * @param {number} rowId
1313
+ * @param {Object} rowData - Column heading → value pairs
1314
+ * @returns {Promise<Object>}
1315
+ */
1316
+ async editDatasetRow(dataSetId, rowId, rowData) {
1317
+ return this.put(`/dataset/data/${dataSetId}/${rowId}`, rowData);
1318
+ }
1319
+
1320
+ /**
1321
+ * Delete a dataset row
1322
+ * @param {number} dataSetId
1323
+ * @param {number} rowId
1324
+ * @returns {Promise<void>}
1325
+ */
1326
+ async deleteDatasetRow(dataSetId, rowId) {
1327
+ await this.del(`/dataset/data/${dataSetId}/${rowId}`);
1328
+ }
1329
+
1330
+ /**
1331
+ * Import CSV data into a dataset
1332
+ * @param {number} dataSetId
1333
+ * @param {FormData} formData - Must include CSV file
1334
+ * @returns {Promise<Object>}
1335
+ */
1336
+ async importDatasetCsv(dataSetId, formData) {
1337
+ return this.requestMultipart('POST', `/dataset/import/${dataSetId}`, formData);
1338
+ }
1339
+
1340
+ /**
1341
+ * Clear all data from a dataset
1342
+ * @param {number} dataSetId
1343
+ * @returns {Promise<void>}
1344
+ */
1345
+ async clearDataset(dataSetId) {
1346
+ await this.del(`/dataset/data/${dataSetId}`);
1347
+ }
1348
+
1349
+ // ── Notification CRUD (#29) ────────────────────────────────────────
1350
+
1351
+ /**
1352
+ * List notifications
1353
+ * @param {Object} [filters] - Filters (notificationId, subject)
1354
+ * @returns {Promise<Array>}
1355
+ */
1356
+ async listNotifications(filters = {}) {
1357
+ const data = await this.get('/notification', filters);
1358
+ return Array.isArray(data) ? data : [];
1359
+ }
1360
+
1361
+ /**
1362
+ * Create a notification
1363
+ * @param {Object} params - { subject, body, isEmail, isInterrupt, displayGroupIds, ... }
1364
+ * @returns {Promise<Object>}
1365
+ */
1366
+ async createNotification(params) {
1367
+ return this.post('/notification', params);
1368
+ }
1369
+
1370
+ /**
1371
+ * Edit a notification
1372
+ * @param {number} notificationId
1373
+ * @param {Object} params
1374
+ * @returns {Promise<Object>}
1375
+ */
1376
+ async editNotification(notificationId, params) {
1377
+ return this.put(`/notification/${notificationId}`, params);
1378
+ }
1379
+
1380
+ /**
1381
+ * Delete a notification
1382
+ * @param {number} notificationId
1383
+ * @returns {Promise<void>}
1384
+ */
1385
+ async deleteNotification(notificationId) {
1386
+ await this.del(`/notification/${notificationId}`);
1387
+ }
1388
+
1389
+ // ── Folder CRUD (#30) ─────────────────────────────────────────────
1390
+
1391
+ /**
1392
+ * List folders
1393
+ * @param {Object} [filters] - Filters (folderId, text)
1394
+ * @returns {Promise<Array>}
1395
+ */
1396
+ async listFolders(filters = {}) {
1397
+ const data = await this.get('/folder', filters);
1398
+ return Array.isArray(data) ? data : [];
1399
+ }
1400
+
1401
+ /**
1402
+ * Create a folder
1403
+ * @param {Object} params - { text, parentId }
1404
+ * @returns {Promise<Object>}
1405
+ */
1406
+ async createFolder(params) {
1407
+ return this.post('/folder', params);
1408
+ }
1409
+
1410
+ /**
1411
+ * Edit a folder
1412
+ * @param {number} folderId
1413
+ * @param {Object} params
1414
+ * @returns {Promise<Object>}
1415
+ */
1416
+ async editFolder(folderId, params) {
1417
+ return this.put(`/folder/${folderId}`, params);
1418
+ }
1419
+
1420
+ /**
1421
+ * Delete a folder
1422
+ * @param {number} folderId
1423
+ * @returns {Promise<void>}
1424
+ */
1425
+ async deleteFolder(folderId) {
1426
+ await this.del(`/folder/${folderId}`);
1427
+ }
1428
+
1429
+ // ── Tag CRUD + Entity Tagging (#31) ────────────────────────────────
1430
+
1431
+ /**
1432
+ * List tags
1433
+ * @param {Object} [filters] - Filters (tagId, tag)
1434
+ * @returns {Promise<Array>}
1435
+ */
1436
+ async listTags(filters = {}) {
1437
+ const data = await this.get('/tag', filters);
1438
+ return Array.isArray(data) ? data : [];
1439
+ }
1440
+
1441
+ /**
1442
+ * Create a tag
1443
+ * @param {Object} params - { tag }
1444
+ * @returns {Promise<Object>}
1445
+ */
1446
+ async createTag(params) {
1447
+ return this.post('/tag', params);
1448
+ }
1449
+
1450
+ /**
1451
+ * Edit a tag
1452
+ * @param {number} tagId
1453
+ * @param {Object} params
1454
+ * @returns {Promise<Object>}
1455
+ */
1456
+ async editTag(tagId, params) {
1457
+ return this.put(`/tag/${tagId}`, params);
1458
+ }
1459
+
1460
+ /**
1461
+ * Delete a tag
1462
+ * @param {number} tagId
1463
+ * @returns {Promise<void>}
1464
+ */
1465
+ async deleteTag(tagId) {
1466
+ await this.del(`/tag/${tagId}`);
1467
+ }
1468
+
1469
+ /**
1470
+ * Tag an entity (layout, media, campaign, etc.)
1471
+ * @param {string} entity - Entity type (layout, media, campaign, displaygroup, playlist)
1472
+ * @param {number} id - Entity ID
1473
+ * @param {string[]} tags - Tag names
1474
+ * @returns {Promise<void>}
1475
+ */
1476
+ async tagEntity(entity, id, tags) {
1477
+ await this.post(`/${entity}/${id}/tag`, { tag: tags.join(',') });
1478
+ }
1479
+
1480
+ /**
1481
+ * Remove tags from an entity
1482
+ * @param {string} entity - Entity type
1483
+ * @param {number} id - Entity ID
1484
+ * @param {string[]} tags - Tag names to remove
1485
+ * @returns {Promise<void>}
1486
+ */
1487
+ async untagEntity(entity, id, tags) {
1488
+ await this.post(`/${entity}/${id}/untag`, { tag: tags.join(',') });
1489
+ }
1490
+
1491
+ // ── DisplayGroup Actions (#32) ─────────────────────────────────────
1492
+
1493
+ /**
1494
+ * Change the layout on a display group (immediate override)
1495
+ * @param {number} displayGroupId
1496
+ * @param {number} layoutId
1497
+ * @returns {Promise<void>}
1498
+ */
1499
+ async dgChangeLayout(displayGroupId, layoutId) {
1500
+ await this.post(`/displaygroup/${displayGroupId}/action/changeLayout`, { layoutId });
1501
+ }
1502
+
1503
+ /**
1504
+ * Overlay a layout on a display group
1505
+ * @param {number} displayGroupId
1506
+ * @param {number} layoutId
1507
+ * @returns {Promise<void>}
1508
+ */
1509
+ async dgOverlayLayout(displayGroupId, layoutId) {
1510
+ await this.post(`/displaygroup/${displayGroupId}/action/overlayLayout`, { layoutId });
1511
+ }
1512
+
1513
+ /**
1514
+ * Revert a display group to its scheduled content
1515
+ * @param {number} displayGroupId
1516
+ * @returns {Promise<void>}
1517
+ */
1518
+ async dgRevertToSchedule(displayGroupId) {
1519
+ await this.post(`/displaygroup/${displayGroupId}/action/revertToSchedule`);
1520
+ }
1521
+
1522
+ /**
1523
+ * Trigger immediate content collection on a display group
1524
+ * @param {number} displayGroupId
1525
+ * @returns {Promise<void>}
1526
+ */
1527
+ async dgCollectNow(displayGroupId) {
1528
+ await this.post(`/displaygroup/${displayGroupId}/action/collectNow`);
1529
+ }
1530
+
1531
+ /**
1532
+ * Send a command to a display group
1533
+ * @param {number} displayGroupId
1534
+ * @param {number} commandId
1535
+ * @returns {Promise<void>}
1536
+ */
1537
+ async dgSendCommand(displayGroupId, commandId) {
1538
+ await this.post(`/displaygroup/${displayGroupId}/action/command`, { commandId });
1539
+ }
1540
+
1541
+ /**
1542
+ * Edit display group properties
1543
+ * @param {number} displayGroupId
1544
+ * @param {Object} params - Properties to update
1545
+ * @returns {Promise<Object>}
1546
+ */
1547
+ async editDisplayGroup(displayGroupId, params) {
1548
+ return this.put(`/displaygroup/${displayGroupId}`, params);
1549
+ }
750
1550
  }
751
1551
 
752
1552
  /**