diff --git a/.github/workflows/buildjs.yml b/.github/workflows/buildjs.yml index a6daad3..3535edf 100644 --- a/.github/workflows/buildjs.yml +++ b/.github/workflows/buildjs.yml @@ -3,7 +3,10 @@ name: buildjs permissions: - contents: write # Grants write access to repository content + contents: write + issues: read + checks: write + pull-requests: write on: workflow_call: inputs: diff --git a/src/broadcast-solution/Controls/fdn_Broadcast.AppModulePicker/ControlManifest.xml b/src/broadcast-solution/Controls/fdn_Broadcast.AppModulePicker/ControlManifest.xml index 564126c..fcaf640 100644 --- a/src/broadcast-solution/Controls/fdn_Broadcast.AppModulePicker/ControlManifest.xml +++ b/src/broadcast-solution/Controls/fdn_Broadcast.AppModulePicker/ControlManifest.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/Entity.xml b/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/Entity.xml index e9870b8..fcdfa9b 100644 --- a/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/Entity.xml +++ b/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/Entity.xml @@ -140,6 +140,69 @@ + + picklist + fdn_actiontype + fdn_actiontype + none + ValidForAdvancedFind|ValidForForm|ValidForGrid + auto + 1 + 1 + 1 + 1 + 1 + 0 + 1.0.0.0 + 1 + 1 + 1 + 1 + 1 + 0 + 0 + 0 + 1 + 1 + 0 + + 0 + 0 + 0 + 0 + -1 + + picklist + 1.0.0.0 + 1 + + + + + + + + + + + + + + + + + + + + nvarchar fdn_appmoduleid @@ -220,6 +283,90 @@ + + nvarchar + fdn_buttonactionurl + fdn_buttonactionurl + none + ValidForAdvancedFind|ValidForForm|ValidForGrid + auto + 1 + 1 + 1 + 1 + 1 + 0 + 1.0 + 1 + 1 + 1 + 1 + 1 + 0 + 0 + 0 + 1 + 1 + 0 + + 0 + 0 + 0 + 0 + url + 500 + 1000 + + + + + + + + + + + nvarchar + fdn_buttondefaulttext + fdn_buttondefaulttext + none + ValidForAdvancedFind|ValidForForm|ValidForGrid + auto + 1 + 1 + 1 + 1 + 1 + 0 + 1.0 + 1 + 1 + 1 + 1 + 1 + 0 + 0 + 0 + 1 + 1 + 0 + + 0 + 0 + 0 + 0 + text + 100 + 200 + + + + + + + + + nvarchar fdn_description diff --git a/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/FormXml/main/{e457ccdb-b279-49d1-ae85-1ffae404cc55}.xml b/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/FormXml/main/{e457ccdb-b279-49d1-ae85-1ffae404cc55}.xml index 83d637c..4a30ae6 100644 --- a/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/FormXml/main/{e457ccdb-b279-49d1-ae85-1ffae404cc55}.xml +++ b/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/FormXml/main/{e457ccdb-b279-49d1-ae85-1ffae404cc55}.xml @@ -149,6 +149,63 @@ + + + + + + +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
@@ -240,6 +297,16 @@ + + + + + + + + + + 1 1 diff --git a/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/FormXml/main/{e457ccdb-b279-49d1-ae85-1ffae404cc55}_managed.xml b/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/FormXml/main/{e457ccdb-b279-49d1-ae85-1ffae404cc55}_managed.xml index e8848e7..d2b7820 100644 --- a/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/FormXml/main/{e457ccdb-b279-49d1-ae85-1ffae404cc55}_managed.xml +++ b/src/broadcast-solution/Entities/fdn_BroadcastAppNotification/FormXml/main/{e457ccdb-b279-49d1-ae85-1ffae404cc55}_managed.xml @@ -41,6 +41,7 @@ @@ -141,6 +142,60 @@ + + + + + + +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
@@ -229,6 +284,16 @@ + + + + + + + + + + 1 1 diff --git a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/Entity.xml b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/Entity.xml index 6c46422..c6b9d9d 100644 --- a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/Entity.xml +++ b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/Entity.xml @@ -140,12 +140,55 @@ + + nvarchar + fdn_actionbuttondisplaytext + fdn_actionbuttondisplaytext + none + ValidForAdvancedFind|ValidForForm|ValidForGrid + auto + 1 + 1 + 1 + 1 + 1 + 0 + 1.0 + 1 + 1 + 1 + 1 + 1 + 0 + 0 + 0 + 1 + 1 + 0 + + 0 + 0 + 0 + 0 + text + 100 + 200 + + + + + + + + + lookup fdn_appnotificationconfigid fdn_appnotificationconfigid required ValidForAdvancedFind|ValidForForm|ValidForGrid + auto 1 1 1 diff --git a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/main/{16ac9028-5ff7-48d4-99cc-a7560b2d828f}.xml b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/main/{16ac9028-5ff7-48d4-99cc-a7560b2d828f}.xml index 742c24b..b626160 100644 --- a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/main/{16ac9028-5ff7-48d4-99cc-a7560b2d828f}.xml +++ b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/main/{16ac9028-5ff7-48d4-99cc-a7560b2d828f}.xml @@ -63,6 +63,23 @@ +
+ + + + + + + + + + + + +
diff --git a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/main/{16ac9028-5ff7-48d4-99cc-a7560b2d828f}_managed.xml b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/main/{16ac9028-5ff7-48d4-99cc-a7560b2d828f}_managed.xml index 5b7c417..8f57140 100644 --- a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/main/{16ac9028-5ff7-48d4-99cc-a7560b2d828f}_managed.xml +++ b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/main/{16ac9028-5ff7-48d4-99cc-a7560b2d828f}_managed.xml @@ -59,6 +59,22 @@ +
+ + + + + + + + + + + + +
diff --git a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/quickCreate/{93dbb73e-9d86-f011-b4cc-6045bdcd660b}.xml b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/quickCreate/{93dbb73e-9d86-f011-b4cc-6045bdcd660b}.xml index 57a28b4..8e949e0 100644 --- a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/quickCreate/{93dbb73e-9d86-f011-b4cc-6045bdcd660b}.xml +++ b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/quickCreate/{93dbb73e-9d86-f011-b4cc-6045bdcd660b}.xml @@ -59,6 +59,15 @@ + + + + + + + diff --git a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/quickCreate/{93dbb73e-9d86-f011-b4cc-6045bdcd660b}_managed.xml b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/quickCreate/{93dbb73e-9d86-f011-b4cc-6045bdcd660b}_managed.xml index b6a0ed2..cd2efae 100644 --- a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/quickCreate/{93dbb73e-9d86-f011-b4cc-6045bdcd660b}_managed.xml +++ b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/FormXml/quickCreate/{93dbb73e-9d86-f011-b4cc-6045bdcd660b}_managed.xml @@ -23,6 +23,7 @@ @@ -31,6 +32,7 @@ @@ -39,6 +41,7 @@ @@ -47,6 +50,7 @@ @@ -55,6 +59,14 @@ + + + + + + + diff --git a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/SavedQueries/{b0a3afae-2afe-463a-a657-cf9725b3dccf}.xml b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/SavedQueries/{b0a3afae-2afe-463a-a657-cf9725b3dccf}.xml index e09e79c..0ce2f9c 100644 --- a/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/SavedQueries/{b0a3afae-2afe-463a-a657-cf9725b3dccf}.xml +++ b/src/broadcast-solution/Entities/fdn_LocalizedNotificationContent/SavedQueries/{b0a3afae-2afe-463a-a657-cf9725b3dccf}.xml @@ -12,6 +12,7 @@ + @@ -25,6 +26,7 @@ + diff --git a/src/broadcast-solution/Other/Customizations.xml b/src/broadcast-solution/Other/Customizations.xml index 5a97164..690b286 100644 --- a/src/broadcast-solution/Other/Customizations.xml +++ b/src/broadcast-solution/Other/Customizations.xml @@ -1,5 +1,5 @@  - + diff --git a/src/broadcast-solution/Other/Solution.xml b/src/broadcast-solution/Other/Solution.xml index 113e1fc..7cadda8 100644 --- a/src/broadcast-solution/Other/Solution.xml +++ b/src/broadcast-solution/Other/Solution.xml @@ -1,5 +1,5 @@  - + BroadcastAppNotificationsSolution diff --git a/src/broadcast-solution/WebResources/fdn_/scripts/broadcast.js b/src/broadcast-solution/WebResources/fdn_/scripts/broadcast.js index daa3d2f..7d9ea2f 100644 --- a/src/broadcast-solution/WebResources/fdn_/scripts/broadcast.js +++ b/src/broadcast-solution/WebResources/fdn_/scripts/broadcast.js @@ -1,4 +1,4 @@ -(function(){"use strict";function g(n,e,t){I(n,e,t,"get"),I(n,e,t,"value")}function I(n,e,t,o){if(typeof t[o]=="function"){const i=Symbol(e),a=t[o];t[o]=function(...r){let d=this,l=d[i];return typeof l>"u"&&(l=d[i]=a.apply(this,r)),l}}}class c{constructor(e,t){this._cultures={1033:e,1036:t}}toString(){const e=Xrm.Utility.getGlobalContext().userSettings.languageId;return this._cultures[e]||this._cultures[1033]}}const s={general:{MESSAGE_RECORD_DIRTY:new c("You have pending changes, please save the record.","Vous avez des modifications en attente de sauvegarde. Veuillez sauvegarder."),WAIT_POPUPTITLE:new c("Wait!","Attendez!"),CANCEL_BTN:new c("Cancel","Annuler"),PROCESSING:new c("Processing...","En traitement...")},broadcast:{PUBLISH_BTN:new c("Publish","Publier"),UNPUBLISH_BTN:new c("Unpublish","Dépublier"),NOTIFICATION_CONFIRMPUBLISH_TITLE:new c("Confirm Publish","Confirmer la publication"),NOTIFICATION_CONFIRMPUBLISH_CONTENT:new c(`Are you sure you want to publish this notification?\r +(function(){"use strict";function f(e,t,n){y(e,t,n,"get"),y(e,t,n,"value")}function y(e,t,n,i){if(typeof n[i]=="function"){const o=Symbol(t),a=n[i];n[i]=function(...s){let l=this,d=l[o];return typeof d>"u"&&(d=l[o]=a.apply(this,s)),d}}}class c{constructor(t,n){this._cultures={1033:t,1036:n}}toString(){const t=Xrm.Utility.getGlobalContext().userSettings.languageId;return this._cultures[t]||this._cultures[1033]}}const r={general:{MESSAGE_RECORD_DIRTY:new c("You have pending changes, please save the record.","Vous avez des modifications en attente de sauvegarde. Veuillez sauvegarder."),WAIT_POPUPTITLE:new c("Wait!","Attendez!"),CANCEL_BTN:new c("Cancel","Annuler"),PROCESSING:new c("Processing...","En traitement...")},broadcast:{PUBLISH_BTN:new c("Publish","Publier"),UNPUBLISH_BTN:new c("Unpublish","Dépublier"),NOTIFICATION_CONFIRMPUBLISH_TITLE:new c("Confirm Publish","Confirmer la publication"),NOTIFICATION_CONFIRMPUBLISH_CONTENT:new c(`Are you sure you want to publish this notification?\r \r This notification will be shown to users.`,`Êtes-vous sûr de vouloir publier cette notification?\r \r @@ -6,5 +6,5 @@ Cette notification sera affichée aux utilisateurs.`),NOTIFICATION_CONFIRMUNPUBL \r This notification will no longer be shown to users.`,`Êtes-vous sûr de vouloir dépublier cette notification?\r \r -Cette notification ne sera plus affichée aux utilisateurs.`)}};function N(n){return n.replace("{","").replace("}","")}async function A(n){return n.data.entity.getIsDirty()?(await Xrm.Navigation.openAlertDialog({title:s.general.WAIT_POPUPTITLE.toString(),text:s.general.MESSAGE_RECORD_DIRTY.toString()}),!0):!1}function C(n,e,t,o){n.getAttribute(e)?.addOnChange(t.bind(o))}function w(){switch(Xrm.Utility.getGlobalContext().userSettings.languageId){case 1036:return 794560002;default:return 794560001}}class U{constructor(e,t){this.webApi=e,this._storage=t,this._cacheKey="broadcast_published_{appid}",this._expirationInSeconds=60*5,this._userLang=w()}_getExpiration(){return Date.now()+this._expirationInSeconds*1e3}async getPublishedNotifications(e){const t=this._cacheKey.replace("{appid}",e),o=this._storage.getItem(t),i=Date.now();if(o){const P=JSON.parse(o);if(P.timestamp>i)return P.value;this._storage.removeItem(t)}const a=`&$filter=statuscode eq 2 and fdn_appmoduleid eq '${e}'&$orderby=fdn_level asc`,l=`?$select=${["fdn_appmoduleid","fdn_message","fdn_broadcastappnotificationid","fdn_level"].join(",")}`+"&$expand=fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification($select=fdn_contentmessage,fdn_language;$filter=statecode eq 0)"+a,p=(await this.webApi.retrieveMultipleRecords("fdn_broadcastappnotification",l)).entities,m={value:p.map(v),timestamp:this._getExpiration()};return this._storage.setItem(t,JSON.stringify(m)),p}async Publish(e){await Xrm.WebApi.updateRecord("fdn_broadcastappnotification",e,{statecode:1,statuscode:2})}async Unpublish(e){await Xrm.WebApi.updateRecord("fdn_broadcastappnotification",e,{statecode:0,statuscode:1})}}function v(n){let t=T(n,["fdn_appmoduleid","fdn_message","fdn_broadcastappnotificationid","fdn_level","fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification"]);const o=t.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification;return!o||o.length===0||o.map(i=>T(i,["fdn_contentmessage","fdn_language"])),t}function T(n,e){for(const t in n)e.includes(t)||delete n[t];return n}class L{constructor(e){this._storage=e,this._storageCacheKey="broascast_{appid}"}getCachedNotifications(e){const t=this._storageCacheKey.replace("{appid}",e),o=this._storage.getItem(t);return o?JSON.parse(o):[]}setCachedNotifications(e,t){const o=this._storageCacheKey.replace("{appid}",e),i=t??[];this._storage.setItem(o,JSON.stringify(i))}}const y=new L(window.localStorage),_=new U(Xrm.WebApi,window.sessionStorage);var u=(n=>(n[n.Success=79456e4]="Success",n[n.Danger=794560001]="Danger",n[n.Warning=794560002]="Warning",n[n.Information=794560003]="Information",n))(u||{});class E{async renderNotifications(e){try{const t=Xrm.Utility.getPageContext()?.input?.pageType;if(!t||t!==e)return!1;const o=w();console.log("broadcast renderNotifications");const i=await Xrm.Utility.getGlobalContext().getCurrentAppProperties();if(!i||!i.appId)return!1;const r=y.getCachedNotifications(i.appId).map(f=>Xrm.App.clearGlobalNotification(f.uid)),d=await _.getPublishedNotifications(i.appId);await Promise.all(r);const l=[];for(const f of d){const p=R(f,o),m=await Xrm.App.addGlobalNotification(p);l.push({uid:m,data:f})}y.setCachedNotifications(i.appId,l)}catch(t){console.error(t)}finally{return!1}}}function R(n,e){const t=B(n.fdn_level);let o=n.fdn_message;const i=n.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification;return i&&i.length>0&&(o=i.find(a=>a.fdn_language===e)?.fdn_contentmessage??n.fdn_message),{level:t,message:o,showCloseButton:!1,type:2}}function B(n){switch(n){case u.Success:return 1;case u.Danger:return 2;case u.Warning:return 3;case u.Information:return 4;default:return 4}}class D{async PublishNotification(e){const t=N(e.data.entity.getId());if(await A(e))return;const i={text:s.broadcast.NOTIFICATION_CONFIRMPUBLISH_CONTENT.toString(),title:s.broadcast.NOTIFICATION_CONFIRMPUBLISH_TITLE.toString(),confirmButtonLabel:s.broadcast.PUBLISH_BTN.toString(),cancelButtonLabel:s.general.CANCEL_BTN.toString()};(await Xrm.Navigation.openConfirmDialog(i)).confirmed&&(Xrm.Utility.showProgressIndicator(s.general.PROCESSING.toString()),await _.Publish(t),e.data.refresh(!1),e.ui.refreshRibbon(!0),Xrm.Utility.closeProgressIndicator())}async UnpublishNotification(e){const t=N(e.data.entity.getId()),o={text:s.broadcast.NOTIFICATION_CONFIRMUNPUBLISH_CONTENT.toString(),title:s.broadcast.NOTIFICATION_CONFIRMUNPUBLISH_TITLE.toString(),confirmButtonLabel:s.broadcast.UNPUBLISH_BTN.toString(),cancelButtonLabel:s.general.CANCEL_BTN.toString()};(await Xrm.Navigation.openConfirmDialog(o)).confirmed&&(Xrm.Utility.showProgressIndicator(s.general.PROCESSING.toString()),await _.Unpublish(t),e.data.refresh(!1),e.ui.refreshRibbon(!0),Xrm.Utility.closeProgressIndicator())}}var F=Object.defineProperty,X=Object.getOwnPropertyDescriptor,S=(n,e,t,o)=>{for(var i=X(e,t),a=n.length-1,r;a>=0;a--)(r=n[a])&&(i=r(e,t,i)||i);return i&&F(e,t,i),i};class h{get application(){return new E}get fdn_broadcastappnotification(){return new D}}S([g],h.prototype,"application"),S([g],h.prototype,"fdn_broadcastappnotification");const x=new h;class M{async onLoad(e){const t=e.getFormContext();C(t,"fdn_language",this.onLanguageChange,this),C(t,"fdn_appnotificationconfigid",this.onConfigChange,this),t.ui.getFormType()==XrmEnum.FormType.Create&&b(t)}async onLanguageChange(e){const t=e.getFormContext();b(t)}async onConfigChange(e){const t=e.getFormContext();b(t)}}function b(n){const e=n.getAttribute("fdn_language"),t=n.getAttribute("fdn_appnotificationconfigid"),o=n.getAttribute("fdn_name"),a=(t.getValue()?t.getValue()[0]:null)?.name??"unknown",r=e.getSelectedOption();o.setValue(`${r.text} - ${a}`)}var z=Object.defineProperty,G=Object.getOwnPropertyDescriptor,H=(n,e,t,o)=>{for(var i=G(e,t),a=n.length-1,r;a>=0;a--)(r=n[a])&&(i=r(e,t,i)||i);return i&&z(e,t,i),i};class O{get fdn_localizednotificationcontent(){return new M}}H([g],O.prototype,"fdn_localizednotificationcontent");const $=new O,V={ribbon:x,forms:$};if(typeof window<"u"){const n=globalThis.XrmEnum??{FormType:{Undefined:0,Create:1,Update:2,ReadOnly:3,Disabled:4,QuickCreate:5,BulkEdit:6,ReadOptimized:11}};globalThis.XrmEnum=n,window.broadcast=window.broadcast??V}})(); +Cette notification ne sera plus affichée aux utilisateurs.`)}};function C(e){return e.replace("{","").replace("}","")}async function F(e){return e.data.entity.getIsDirty()?(await Xrm.Navigation.openAlertDialog({title:r.general.WAIT_POPUPTITLE.toString(),text:r.general.MESSAGE_RECORD_DIRTY.toString()}),!0):!1}function p(e,t,n,i){e.getAttribute(t)?.addOnChange(n.bind(i))}var u=(e=>(e[e.Success=79456e4]="Success",e[e.Danger=794560001]="Danger",e[e.Warning=794560002]="Warning",e[e.Information=794560003]="Information",e))(u||{}),g=(e=>(e[e.English=794560001]="English",e[e.French=794560002]="French",e))(g||{}),_=(e=>(e[e.WebLink=79456e4]="WebLink",e))(_||{});function S(){switch(Xrm.Utility.getGlobalContext().userSettings.languageId){case 1036:return g.French;default:return g.English}}class R{constructor(t,n){this.webApi=t,this._storage=n,this._cacheKey="broadcast_published_{appid}",this._expirationInSeconds=60*5,this._userLang=S()}_getExpiration(){return Date.now()+this._expirationInSeconds*1e3}async getPublishedNotifications(t){const n=this._cacheKey.replace("{appid}",t),i=this._storage.getItem(n),o=Date.now();if(i){const U=JSON.parse(i);if(U.timestamp>o)return U.value;this._storage.removeItem(n)}const a=`&$filter=statuscode eq 2 and fdn_appmoduleid eq '${t}'&$orderby=fdn_level asc`,s=["fdn_appmoduleid","fdn_message","fdn_broadcastappnotificationid","fdn_level","fdn_buttondefaulttext","fdn_buttonactionurl","fdn_actiontype"],d=`&$expand=fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification($select=${["fdn_language","fdn_contentmessage","fdn_actionbuttondisplaytext"].join(",")};$filter=statecode eq 0)`,N=`?$select=${s.join(",")}`+d+a,E=(await this.webApi.retrieveMultipleRecords("fdn_broadcastappnotification",N)).entities,Y={value:E.map(x),timestamp:this._getExpiration()};return this._storage.setItem(n,JSON.stringify(Y)),E}async Publish(t){await Xrm.WebApi.updateRecord("fdn_broadcastappnotification",t,{statecode:1,statuscode:2})}async Unpublish(t){await Xrm.WebApi.updateRecord("fdn_broadcastappnotification",t,{statecode:0,statuscode:1})}}function x(e){let n=T(e,["fdn_appmoduleid","fdn_message","fdn_broadcastappnotificationid","fdn_level","fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification","fdn_buttondefaulttext","fdn_buttonactionurl","fdn_actiontype"]);const i=n.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification;return!i||i.length===0||i.map(o=>T(o,["fdn_contentmessage","fdn_language","fdn_actionbuttondisplaytext"])),n}function T(e,t){for(const n in e)t.includes(n)||delete e[n];return e}class B{constructor(t){this._storage=t,this._storageCacheKey="broascast_{appid}"}getCachedNotifications(t){const n=this._storageCacheKey.replace("{appid}",t),i=this._storage.getItem(n);return i?JSON.parse(i):[]}setCachedNotifications(t,n){const i=this._storageCacheKey.replace("{appid}",t),o=n??[];this._storage.setItem(i,JSON.stringify(o))}}const D={[u.Success.toString()]:1,[u.Danger.toString()]:2,[u.Warning.toString()]:3,[u.Information.toString()]:4};class X{constructor(){this._buildAction=(t,n)=>{if(t.fdn_actiontype==_.WebLink)return{actionLabel:n?.fdn_actionbuttondisplaytext??t.fdn_buttondefaulttext,eventHandler:()=>{window.open(t.fdn_buttonactionurl,"_blank")}}}}create(t){const n=S(),i=D[t.fdn_level.toString()];let o=t.fdn_message;const a=t.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification,s=a&&a.length>0?a.find(l=>l.fdn_language===n):void 0;return s!=null&&(o=s.fdn_contentmessage??t.fdn_message),{level:i,message:o,showCloseButton:!1,type:2,action:this._buildAction(t,s)}}}const M=new X,w=new B(window.localStorage),b=new R(Xrm.WebApi,window.sessionStorage);class ${async renderNotifications(t){try{const n=Xrm.Utility.getPageContext()?.input?.pageType;if(!n||n!==t)return!1;console.log("broadcast renderNotifications");const i=await Xrm.Utility.getGlobalContext().getCurrentAppProperties();if(!i||!i.appId)return!1;const a=w.getCachedNotifications(i.appId).map(d=>Xrm.App.clearGlobalNotification(d.uid)),s=await b.getPublishedNotifications(i.appId);await Promise.all(a);const l=[];for(const d of s){const N=M.create(d),v=await Xrm.App.addGlobalNotification(N);l.push({uid:v,data:d})}w.setCachedNotifications(i.appId,l)}catch(n){console.error(n)}finally{return!1}}}class z{async PublishNotification(t){const n=C(t.data.entity.getId());if(await F(t))return;const o={text:r.broadcast.NOTIFICATION_CONFIRMPUBLISH_CONTENT.toString(),title:r.broadcast.NOTIFICATION_CONFIRMPUBLISH_TITLE.toString(),confirmButtonLabel:r.broadcast.PUBLISH_BTN.toString(),cancelButtonLabel:r.general.CANCEL_BTN.toString()};(await Xrm.Navigation.openConfirmDialog(o)).confirmed&&(Xrm.Utility.showProgressIndicator(r.general.PROCESSING.toString()),await b.Publish(n),t.data.refresh(!1),t.ui.refreshRibbon(!0),Xrm.Utility.closeProgressIndicator())}async UnpublishNotification(t){const n=C(t.data.entity.getId()),i={text:r.broadcast.NOTIFICATION_CONFIRMUNPUBLISH_CONTENT.toString(),title:r.broadcast.NOTIFICATION_CONFIRMUNPUBLISH_TITLE.toString(),confirmButtonLabel:r.broadcast.UNPUBLISH_BTN.toString(),cancelButtonLabel:r.general.CANCEL_BTN.toString()};(await Xrm.Navigation.openConfirmDialog(i)).confirmed&&(Xrm.Utility.showProgressIndicator(r.general.PROCESSING.toString()),await b.Unpublish(n),t.data.refresh(!1),t.ui.refreshRibbon(!0),Xrm.Utility.closeProgressIndicator())}}var H=Object.defineProperty,G=Object.getOwnPropertyDescriptor,O=(e,t,n,i)=>{for(var o=G(t,n),a=e.length-1,s;a>=0;a--)(s=e[a])&&(o=s(t,n,o)||o);return o&&H(t,n,o),o};class h{get application(){return new $}get fdn_broadcastappnotification(){return new z}}O([f],h.prototype,"application"),O([f],h.prototype,"fdn_broadcastappnotification");const V=new h;class W{async onLoad(t){const n=t.getFormContext();p(n,"fdn_language",this.onLanguageChange,this),p(n,"fdn_appnotificationconfigid",this.onConfigChange,this),n.ui.getFormType()==XrmEnum.FormType.Create&&m(n)}async onLanguageChange(t){const n=t.getFormContext();m(n)}async onConfigChange(t){const n=t.getFormContext();m(n)}}function m(e){const t=e.getAttribute("fdn_language"),n=e.getAttribute("fdn_appnotificationconfigid"),i=e.getAttribute("fdn_name"),a=(n.getValue()?n.getValue()[0]:null)?.name??"unknown",s=t.getSelectedOption();i.setValue(`${s.text} - ${a}`)}const A={[_.WebLink.toString()]:{name:"tab_action_section_url",mandatoryFields:["fdn_buttondefaulttext","fdn_buttonactionurl"]}};class k{async onLoad(t){const n=t.getFormContext();p(n,"fdn_actiontype",this.onActionTypeChange,this),P(n)}async onActionTypeChange(t){const n=t.getFormContext();P(n)}}function P(e){const t=e.getAttribute("fdn_actiontype"),n=e.ui.tabs.get("tab_action"),i=t.getValue()??-1;for(let o in A){const a=A[o],s=i.toString()===o;n.sections.get(a.name)?.setVisible(s),a.mandatoryFields.forEach(l=>{e.getAttribute(l)?.setRequiredLevel(s?"required":"none")})}}var j=Object.defineProperty,K=Object.getOwnPropertyDescriptor,L=(e,t,n,i)=>{for(var o=K(t,n),a=e.length-1,s;a>=0;a--)(s=e[a])&&(o=s(t,n,o)||o);return o&&j(t,n,o),o};class I{get fdn_localizednotificationcontent(){return new W}get fdn_broadcastappnotification(){return new k}}L([f],I.prototype,"fdn_localizednotificationcontent"),L([f],I.prototype,"fdn_broadcastappnotification");const q=new I,J={ribbon:V,forms:q};if(typeof window<"u"){const e=globalThis.XrmEnum??{FormType:{Undefined:0,Create:1,Update:2,ReadOnly:3,Disabled:4,QuickCreate:5,BulkEdit:6,ReadOptimized:11}};globalThis.XrmEnum=e,window.broadcast=window.broadcast??J}})(); //# sourceMappingURL=broadcast.js.map diff --git a/src/broadcast-typescript/app/modules/common/utility/context.ts b/src/broadcast-typescript/app/modules/common/utility/context.ts index d7c962f..5364016 100644 --- a/src/broadcast-typescript/app/modules/common/utility/context.ts +++ b/src/broadcast-typescript/app/modules/common/utility/context.ts @@ -1,9 +1,11 @@ +import { fdn_language } from "@app/modules/domain"; + export function getUserLangFromGlobalContext(){ const userLang = Xrm.Utility.getGlobalContext().userSettings.languageId; switch(userLang){ case 1036: - return 794560002; + return fdn_language.French; default: - return 794560001; + return fdn_language.English; } } \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/domain/entities/fdn_broadcastappnotification.ts b/src/broadcast-typescript/app/modules/domain/entities/fdn_broadcastappnotification.ts index 1fb3bdc..95e8419 100644 --- a/src/broadcast-typescript/app/modules/domain/entities/fdn_broadcastappnotification.ts +++ b/src/broadcast-typescript/app/modules/domain/entities/fdn_broadcastappnotification.ts @@ -1,4 +1,5 @@ import { entity ,fdn_localizednotificationcontent} from "."; +import { fdn_actiontype } from "../optionsets"; export enum fdn_level { Success = 794560000, @@ -8,9 +9,13 @@ export enum fdn_level { } export class fdn_broadcastappnotification extends entity { fdn_level?: fdn_level; + fdn_actiontype?: fdn_actiontype; fdn_message?: string; fdn_name?: string; fdn_appmoduleid?: string fdn_broadcastappnotificationid?: string; + fdn_buttondefaulttext?: string; + fdn_buttonactionurl?: string; fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification?:fdn_localizednotificationcontent[]; + } \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/domain/entities/fdn_localizednotificationcontent.ts b/src/broadcast-typescript/app/modules/domain/entities/fdn_localizednotificationcontent.ts index 18542b6..81691af 100644 --- a/src/broadcast-typescript/app/modules/domain/entities/fdn_localizednotificationcontent.ts +++ b/src/broadcast-typescript/app/modules/domain/entities/fdn_localizednotificationcontent.ts @@ -1,11 +1,12 @@ import { entity } from "."; export enum fdn_language { - English = 794560000, - French = 794560001, + English = 794560001, + French = 794560002, } export type fdn_localizednotificationcontent = entity &{ fdn_localizednotificationcontentid?:string; fdn_language?:fdn_language; fdn_name?:string; fdn_contentmessage?:string; + fdn_actionbuttondisplaytext?:string; } \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/domain/index.ts b/src/broadcast-typescript/app/modules/domain/index.ts index 231fbe0..c626fbf 100644 --- a/src/broadcast-typescript/app/modules/domain/index.ts +++ b/src/broadcast-typescript/app/modules/domain/index.ts @@ -1,2 +1,3 @@ export * from './entities'; -export * from './model'; \ No newline at end of file +export * from './model'; +export * from './optionsets'; \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/domain/optionsets/fdn_actiontype.ts b/src/broadcast-typescript/app/modules/domain/optionsets/fdn_actiontype.ts new file mode 100644 index 0000000..6e2fff5 --- /dev/null +++ b/src/broadcast-typescript/app/modules/domain/optionsets/fdn_actiontype.ts @@ -0,0 +1,3 @@ +export enum fdn_actiontype { + WebLink = 794560000 +} \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/domain/optionsets/index.ts b/src/broadcast-typescript/app/modules/domain/optionsets/index.ts new file mode 100644 index 0000000..3f1f58c --- /dev/null +++ b/src/broadcast-typescript/app/modules/domain/optionsets/index.ts @@ -0,0 +1 @@ +export * from './fdn_actiontype' \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/form/fdn_broadcastappnotification.ts b/src/broadcast-typescript/app/modules/form/fdn_broadcastappnotification.ts new file mode 100644 index 0000000..97aa1e5 --- /dev/null +++ b/src/broadcast-typescript/app/modules/form/fdn_broadcastappnotification.ts @@ -0,0 +1,37 @@ +import * as utilities from "@app/modules/common/utility"; +import { fdn_actiontype } from "@app/modules/domain"; +const ActionTypeSectionMapping = { + [fdn_actiontype.WebLink.toString()]: { + name:"tab_action_section_url", + mandatoryFields:['fdn_buttondefaulttext','fdn_buttonactionurl'] + } +} +export default class { + //broadcast.forms.fdn_broadcastappnotification.onLoad + async onLoad(executionContext: Xrm.Events.LoadEventContext) { + const formContext = executionContext.getFormContext(); + + utilities.registerOnChangeHandler(formContext, 'fdn_actiontype', this.onActionTypeChange, this); + + displayActionSectionAccordingToType(formContext); + } + async onActionTypeChange(executionContext: Xrm.Events.EventContext) { + const formContext = executionContext.getFormContext(); + displayActionSectionAccordingToType(formContext); + } +} +function displayActionSectionAccordingToType(formContext: Xrm.FormContext) { + + const actionTypeAttribute = formContext.getAttribute("fdn_actiontype")!; + const tab = formContext.ui.tabs.get("tab_action")!; + const currentActionType = actionTypeAttribute.getValue() ?? -1; + for(let key in ActionTypeSectionMapping) { + const config = ActionTypeSectionMapping[key as keyof typeof ActionTypeSectionMapping]; + const isSectionVisible = currentActionType.toString() === key; + tab.sections.get(config.name)?.setVisible(isSectionVisible); + config.mandatoryFields.forEach(fieldName => { + formContext.getAttribute(fieldName)?.setRequiredLevel(isSectionVisible ? "required" : "none"); + }); + } + +} \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/form/index.ts b/src/broadcast-typescript/app/modules/form/index.ts index a543784..729a039 100644 --- a/src/broadcast-typescript/app/modules/form/index.ts +++ b/src/broadcast-typescript/app/modules/form/index.ts @@ -1,10 +1,15 @@ import { lazy } from "@app/modules/common/system"; import fdn_localizednotificationcontent from "./fdn_localizednotificationcontent"; +import fdn_broadcastappnotification from "./fdn_broadcastappnotification"; class Forms{ @lazy get fdn_localizednotificationcontent():fdn_localizednotificationcontent{ return new fdn_localizednotificationcontent(); } + @lazy + get fdn_broadcastappnotification():fdn_broadcastappnotification{ + return new fdn_broadcastappnotification(); + } } export const forms = new Forms(); \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/ribbon/application.ts b/src/broadcast-typescript/app/modules/ribbon/application.ts index d02a845..11bf503 100644 --- a/src/broadcast-typescript/app/modules/ribbon/application.ts +++ b/src/broadcast-typescript/app/modules/ribbon/application.ts @@ -1,6 +1,5 @@ -import { notificationStore,broadcastNotificationService } from "@app/modules/services"; -import { fdn_broadcastappnotification, fdn_level, NotificationUI } from "@app/modules/domain"; -import * as utilities from '@app/modules/common/utility'; +import { notificationStore,broadcastNotificationService,clientNotificationFactory } from "@app/modules/services"; +import { NotificationUI } from "@app/modules/domain"; export default class { //broadcast.ribbon.application.renderNotifications async renderNotifications(targetpage?:string){ @@ -12,7 +11,6 @@ export default class { return false; } - const userLang = utilities.getUserLangFromGlobalContext(); console.log("broadcast renderNotifications"); const appProps = await Xrm.Utility.getGlobalContext().getCurrentAppProperties(); if(!appProps || !appProps.appId){ @@ -24,7 +22,7 @@ export default class { await Promise.all(clearTasks); const data:NotificationUI[] = []; for(const n of livenotifications){ - const dataverseNotification = MapNotificationToDataverse(n,userLang); + const dataverseNotification =clientNotificationFactory.create(n); const uid = await Xrm.App.addGlobalNotification(dataverseNotification); data.push({ uid, @@ -42,33 +40,3 @@ export default class { } } -function MapNotificationToDataverse(n:fdn_broadcastappnotification,userLang:number):Xrm.App.Notification{ - const level = MapToLevel(n.fdn_level!); - let msg = n.fdn_message; - const localizedMsgObjs = n.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification; - if(localizedMsgObjs && localizedMsgObjs.length > 0){ - msg = localizedMsgObjs.find((v)=> v.fdn_language === userLang)?.fdn_contentmessage ?? n.fdn_message; - } - return { - level: level, - message: msg!, - showCloseButton: false, - type: 2 - }; -} -function MapToLevel(level:number):number{ - switch(level){ - case fdn_level.Success: - return 1; - case fdn_level.Danger: - return 2; - case fdn_level.Warning: - return 3; - case fdn_level.Information: - return 4; - default: - return 4; - } - - -} \ No newline at end of file diff --git a/src/broadcast-typescript/app/modules/services/BroadcastNotificationService.ts b/src/broadcast-typescript/app/modules/services/BroadcastNotificationService.ts index f42c4d6..08e23d6 100644 --- a/src/broadcast-typescript/app/modules/services/BroadcastNotificationService.ts +++ b/src/broadcast-typescript/app/modules/services/BroadcastNotificationService.ts @@ -36,9 +36,13 @@ export class BroadcastNotificationService implements IBroadcastNotificationServi const columns = ["fdn_appmoduleid", "fdn_message", "fdn_broadcastappnotificationid", - "fdn_level" + "fdn_level", + "fdn_buttondefaulttext", + "fdn_buttonactionurl", + "fdn_actiontype" ]; - const expandOptions = `&$expand=fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification($select=fdn_contentmessage,fdn_language;$filter=statecode eq 0)`; + const expandColumns = ['fdn_language','fdn_contentmessage','fdn_actionbuttondisplaytext']; + const expandOptions = `&$expand=fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification($select=${expandColumns.join(',')};$filter=statecode eq 0)`; const queryOptions = `?$select=${columns.join(',')}`+expandOptions+filterOptions; const result = await this.webApi.retrieveMultipleRecords("fdn_broadcastappnotification", queryOptions); const notifications = result.entities as fdn_broadcastappnotification[]; @@ -63,13 +67,16 @@ function cleanNotification(e:fdn_broadcastappnotification){ "fdn_message", "fdn_broadcastappnotificationid", "fdn_level", - "fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification"]; + "fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification", + "fdn_buttondefaulttext", + "fdn_buttonactionurl", + "fdn_actiontype"]; let n = removeODataGarbages(e,keysToKeep); const lmg = n.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification; if(!lmg || lmg.length === 0){ return n; } - lmg.map(l => removeODataGarbages(l,["fdn_contentmessage","fdn_language"])); + lmg.map(l => removeODataGarbages(l,["fdn_contentmessage","fdn_language","fdn_actionbuttondisplaytext"])); return n; } function removeODataGarbages(e:T,keys:string[]){ diff --git a/src/broadcast-typescript/app/modules/services/XrmAppNotificationFactory.ts b/src/broadcast-typescript/app/modules/services/XrmAppNotificationFactory.ts new file mode 100644 index 0000000..09f8472 --- /dev/null +++ b/src/broadcast-typescript/app/modules/services/XrmAppNotificationFactory.ts @@ -0,0 +1,42 @@ +import { fdn_actiontype, fdn_broadcastappnotification, fdn_level, fdn_localizednotificationcontent } from "@app/modules/domain"; +import * as utilities from '@app/modules/common/utility'; +const LevelMappings = { + [fdn_level.Success.toString()]: 1, + [fdn_level.Danger.toString()]: 2, + [fdn_level.Warning.toString()]: 3, + [fdn_level.Information.toString()]: 4 +}; + +export interface IClientAppNotificationFactory{ + create(n:fdn_broadcastappnotification):Xrm.App.Notification; +} +export class XrmAppNotificationFactory implements IClientAppNotificationFactory{ + private _buildAction = (n:fdn_broadcastappnotification,localizedContent?:fdn_localizednotificationcontent) => { + if(n.fdn_actiontype == fdn_actiontype.WebLink){ + return { + actionLabel:localizedContent?.fdn_actionbuttondisplaytext ?? n.fdn_buttondefaulttext!, + eventHandler: () => { + window.open(n.fdn_buttonactionurl!, "_blank"); + } + } + } + return undefined; + }; + create(n:fdn_broadcastappnotification):Xrm.App.Notification{ + const userLang = utilities.getUserLangFromGlobalContext(); + const level = LevelMappings[n.fdn_level!.toString()]; + let msg = n.fdn_message; + const localizedMsgObjs = n.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification; + const localizedObj = localizedMsgObjs && localizedMsgObjs.length > 0 ? localizedMsgObjs.find((v)=> v.fdn_language === userLang) : undefined; + if(localizedObj !== undefined && localizedObj !== null){ + msg = localizedObj.fdn_contentmessage ?? n.fdn_message; + } + return { + level: level, + message: msg!, + showCloseButton: false, + type: 2, + action: this._buildAction(n,localizedObj) + }; + } +} diff --git a/src/broadcast-typescript/app/modules/services/index.ts b/src/broadcast-typescript/app/modules/services/index.ts index b2ff827..c9f19f7 100644 --- a/src/broadcast-typescript/app/modules/services/index.ts +++ b/src/broadcast-typescript/app/modules/services/index.ts @@ -2,8 +2,9 @@ import { BroadcastNotificationService, IBroadcastNotificationService } from "./B import { INotificationStore,StorageNotificationStore} from "./notifications.store"; export * from "./BroadcastNotificationService"; export * from "./notifications.store"; +import { IClientAppNotificationFactory,XrmAppNotificationFactory} from "./XrmAppNotificationFactory"; - +export const clientNotificationFactory:IClientAppNotificationFactory = new XrmAppNotificationFactory(); export const notificationStore:INotificationStore = new StorageNotificationStore(window.localStorage); export const broadcastNotificationService:IBroadcastNotificationService = new BroadcastNotificationService(Xrm.WebApi,window.sessionStorage); \ No newline at end of file diff --git a/src/broadcast-typescript/tests/XrmTestDriver.ts b/src/broadcast-typescript/tests/XrmTestDriver.ts new file mode 100644 index 0000000..4a73031 --- /dev/null +++ b/src/broadcast-typescript/tests/XrmTestDriver.ts @@ -0,0 +1,69 @@ +import { FormSelectorMock, UserSettingsMock, XrmMockGenerator, XrmStaticMock,ItemCollectionMock,FormItemMock, ContextMock, ClientContextMock, UiMock } from "xrm-mock"; + +export class XrmTestDriver { + + private userSettingsBuilder:XrmUserSettingsBuilder = new XrmUserSettingsBuilder(); + private formBuilder:XrmFormBuilder = new XrmFormBuilder(); + private clientType:Xrm.Client = "Web"; + ConfigureUserSettings(fn:(u:XrmUserSettingsBuilder) => void):XrmTestDriver{ + fn(this.userSettingsBuilder); + return this; + } + ConfigureForm(fn:(f:XrmFormBuilder) => void):XrmTestDriver{ + fn(this.formBuilder); + return this; + } + + public build():XrmStaticMock{ + const xrmMock = XrmMockGenerator.initialise({ + context: new ContextMock({ + clientContext: new ClientContextMock(this.clientType,'Online'), + orgUniqueName: "", + userSettings: this.userSettingsBuilder.build() + }), + ui:new UiMock({ + formSelector: this.formBuilder.build() + }) + }); + return xrmMock; + } +} +class XrmUserSettingsBuilder{ + _userLcid:number = 1033 + PreferFrench():XrmUserSettingsBuilder{ + this._userLcid = 1036; + return this; + } + public build():UserSettingsMock{ + return new UserSettingsMock( + { + isGuidedHelpEnabled: false, + isHighContrastEnabled: false, + isRTL: false, + securityRolePrivileges: [], + securityRoles: [], + userId: "{00000000-0000-0000-0000-000000000000}", + userName: "jdoe", + languageId: this._userLcid + }); + } + +} +class XrmFormBuilder{ + _formType:XrmEnum.FormType = XrmEnum.FormType.Update; + public AsCreateForm():XrmFormBuilder{ + this._formType = XrmEnum.FormType.Create; + return this; + } + public build():FormSelectorMock{ + const itemMocks:ItemCollectionMock = new ItemCollectionMock([ + new FormItemMock({ + id: "form1", + label: "Main Form", + formType:this._formType, + currentItem: true + }), + ]); + return new FormSelectorMock(itemMocks); + } +} \ No newline at end of file diff --git a/src/broadcast-typescript/tests/fakers/fdn_broadcastappnotification.faker.ts b/src/broadcast-typescript/tests/fakers/fdn_broadcastappnotification.faker.ts index a5b4f81..c3f70a8 100644 --- a/src/broadcast-typescript/tests/fakers/fdn_broadcastappnotification.faker.ts +++ b/src/broadcast-typescript/tests/fakers/fdn_broadcastappnotification.faker.ts @@ -1,13 +1,26 @@ -import { fdn_broadcastappnotification, fdn_level } from '@app/modules/domain'; +import { fdn_actiontype, fdn_broadcastappnotification, fdn_level } from '@app/modules/domain'; import { faker } from '@faker-js/faker'; -import { createRandomLocalizedContent } from './fdn_localizednotificationcontent.faker'; -export function createRandomAppNotification(appid:string):fdn_broadcastappnotification{ +import { createBilingualLocalizedContents } from './fdn_localizednotificationcontent.faker'; +export function createRandomAppNotificationWithNoAction(appid:string,localized:boolean = true):fdn_broadcastappnotification{ return { fdn_appmoduleid: appid, fdn_broadcastappnotificationid: faker.string.uuid(), fdn_level: faker.helpers.enumValue(fdn_level), fdn_message: faker.string.alpha({length:10}), fdn_name:faker.string.alpha({length:10}), - fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification: faker.helpers.multiple(createRandomLocalizedContent,{count:2}) + fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification: localized ?createBilingualLocalizedContents():[], + }; +} +export function createRandomAppNotificationWithWebLinkAction(appid:string,localized:boolean = true):fdn_broadcastappnotification{ + return { + fdn_appmoduleid: appid, + fdn_broadcastappnotificationid: faker.string.uuid(), + fdn_level: faker.helpers.enumValue(fdn_level), + fdn_message: faker.string.alpha({length:10}), + fdn_name:faker.string.alpha({length:10}), + fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification:localized? createBilingualLocalizedContents():[], + fdn_actiontype: fdn_actiontype.WebLink, + fdn_buttonactionurl: faker.internet.url(), + fdn_buttondefaulttext: faker.string.alpha({length:10}) }; } \ No newline at end of file diff --git a/src/broadcast-typescript/tests/fakers/fdn_localizednotificationcontent.faker.ts b/src/broadcast-typescript/tests/fakers/fdn_localizednotificationcontent.faker.ts index ede174d..78f59e9 100644 --- a/src/broadcast-typescript/tests/fakers/fdn_localizednotificationcontent.faker.ts +++ b/src/broadcast-typescript/tests/fakers/fdn_localizednotificationcontent.faker.ts @@ -1,10 +1,17 @@ -import { fdn_localizednotificationcontent } from '@app/modules/domain'; +import { fdn_language, fdn_localizednotificationcontent } from '@app/modules/domain'; import { faker } from '@faker-js/faker'; -export function createRandomLocalizedContent():fdn_localizednotificationcontent{ +export function createRandomLocalizedContent(lang:fdn_language = fdn_language.English):fdn_localizednotificationcontent{ return { fdn_contentmessage:faker.string.alpha({length:10}), - fdn_language: faker.number.int({min:794560000,max:794560001}), + fdn_language: lang, fdn_localizednotificationcontentid:faker.string.uuid(), - fdn_name: faker.string.alpha({length:10}) + fdn_name: faker.string.alpha({length:10}), + fdn_actionbuttondisplaytext: faker.string.alpha({length:10}) }; +} +export function createBilingualLocalizedContents():fdn_localizednotificationcontent[]{ + return [ + createRandomLocalizedContent(fdn_language.English), + createRandomLocalizedContent(fdn_language.French) + ]; } \ No newline at end of file diff --git a/src/broadcast-typescript/tests/fakers/notificationui.faker.ts b/src/broadcast-typescript/tests/fakers/notificationui.faker.ts index f415f68..1a7e248 100644 --- a/src/broadcast-typescript/tests/fakers/notificationui.faker.ts +++ b/src/broadcast-typescript/tests/fakers/notificationui.faker.ts @@ -1,9 +1,9 @@ import { NotificationUI } from '@app/modules/domain'; import { faker } from '@faker-js/faker'; -import { createRandomAppNotification } from './fdn_broadcastappnotification.faker'; +import { createRandomAppNotificationWithNoAction } from './fdn_broadcastappnotification.faker'; export function createRandomUiNotification(appid:string) : NotificationUI { return { uid:faker.string.uuid(), - data:createRandomAppNotification(appid) + data:createRandomAppNotificationWithNoAction(appid) }; } \ No newline at end of file diff --git a/src/broadcast-typescript/tests/form/fdn_broadcastappnotification.test.ts b/src/broadcast-typescript/tests/form/fdn_broadcastappnotification.test.ts new file mode 100644 index 0000000..e1c4510 --- /dev/null +++ b/src/broadcast-typescript/tests/form/fdn_broadcastappnotification.test.ts @@ -0,0 +1,80 @@ +import { XrmTestDriver } from "@tests/XrmTestDriver"; +import { OptionSetAttributeMock, SectionMock, StringAttributeMock, TabMock, XrmMockGenerator, XrmStaticMock } from "xrm-mock"; +import { forms } from "@app/modules/form"; +import { fdn_actiontype } from "@app/modules/domain"; + +describe("Given a broadcast app notification Form", () => { + let xrmMock:XrmStaticMock; + let actionTabMock:TabMock; + let urlSectionMock:SectionMock; + let ActionTypeAttributeMock: OptionSetAttributeMock; + let ButtonDisplayTextAttributeMock: StringAttributeMock; + let ButtonActionUrlAttributeMock: StringAttributeMock; + beforeEach(() => { + xrmMock = new XrmTestDriver().ConfigureForm(f => f.AsCreateForm()).build(); + actionTabMock = XrmMockGenerator.Tab.createTab('tab_action'); + urlSectionMock = XrmMockGenerator.Section.createSection("tab_action_section_url","Url",true,actionTabMock); + ActionTypeAttributeMock = XrmMockGenerator.Attribute.createOptionSet("fdn_actiontype",undefined,[{text:"Web Link",value:fdn_actiontype.WebLink}]); + ButtonDisplayTextAttributeMock = XrmMockGenerator.Attribute.createString("fdn_buttondefaulttext"); + ButtonActionUrlAttributeMock = XrmMockGenerator.Attribute.createString("fdn_buttonactionurl"); + }); + describe("When it loads with no actiontype", () => { + beforeEach(() => { + forms.fdn_broadcastappnotification.onLoad(XrmMockGenerator.getEventContext() as any); + }); + it("should add an onChange handler to the action type field", () => { + expect(ActionTypeAttributeMock.eventHandlers).toHaveLength(1); + }); + it("should hide url section", () => { + expect(urlSectionMock.getVisible()).toBeFalsy(); + }); + }); + describe("When it loads with WebLink actiontype", () => { + beforeEach(() => { + ActionTypeAttributeMock.setValue(fdn_actiontype.WebLink); + forms.fdn_broadcastappnotification.onLoad(XrmMockGenerator.getEventContext() as any); + }); + it("should add an onChange handler to the action type field", () => { + expect(ActionTypeAttributeMock.eventHandlers).toHaveLength(1); + }); + it("should display url section", () => { + expect(urlSectionMock.getVisible()).toBeTruthy(); + }); + it("should set button display text field required", () => { + expect(ButtonDisplayTextAttributeMock.getRequiredLevel()).toBe("required"); + }); + it("should set button action url field required", () => { + expect(ButtonActionUrlAttributeMock.getRequiredLevel()).toBe("required"); + }) + }); + describe("When action type changes to WebLink", () => { + beforeEach(() => { + ActionTypeAttributeMock.setValue(fdn_actiontype.WebLink); + forms.fdn_broadcastappnotification.onActionTypeChange(XrmMockGenerator.getEventContext() as any); + }); + it("should display url section", () => { + expect(urlSectionMock.getVisible()).toBeTruthy(); + }); + it("should set button display text field required", () => { + expect(ButtonDisplayTextAttributeMock.getRequiredLevel()).toBe("required"); + }); + it("should set button action url field required", () => { + expect(ButtonActionUrlAttributeMock.getRequiredLevel()).toBe("required"); + }) + }); + describe("When action type changes to none", () => { + beforeEach(() => { + ActionTypeAttributeMock.setValue(null as any); + forms.fdn_broadcastappnotification.onActionTypeChange(XrmMockGenerator.getEventContext() as any); + }); + it("should hide url section", () => { + expect(urlSectionMock.getVisible()).toBeFalsy(); + }); + it("should set button display text field required", () => { + expect(ButtonDisplayTextAttributeMock.getRequiredLevel()).toBe("none"); + }); + it("should set button action url field required", () => { + expect(ButtonActionUrlAttributeMock.getRequiredLevel()).toBe("none"); + }) + }); +}); diff --git a/src/broadcast-typescript/tests/ribbon/application.test.ts b/src/broadcast-typescript/tests/ribbon/application.test.ts index 9db6063..5bcc8dc 100644 --- a/src/broadcast-typescript/tests/ribbon/application.test.ts +++ b/src/broadcast-typescript/tests/ribbon/application.test.ts @@ -1,9 +1,8 @@ import { v4 } from "uuid"; import { MockInstance } from "vitest"; import { XrmMockGenerator, XrmStaticMock } from "xrm-mock"; -import { notificationStore } from "@app/modules/services"; -import { broadcastNotificationService } from "@app/modules/services"; -import { createRandomAppNotification, createRandomUiNotification } from "@tests/fakers"; +import { notificationStore,broadcastNotificationService } from "@app/modules/services"; +import { createRandomAppNotificationWithNoAction, createRandomUiNotification } from "@tests/fakers"; import { faker } from "@faker-js/faker"; import { ribbon } from "@app/modules/ribbon"; @@ -18,7 +17,7 @@ describe("Given an application ribbon",() =>{ let clearGlobalNotifSpy:MockInstance; let getPublishedNotificationSpy:MockInstance; const cachedNotifications = faker.helpers.multiple(() => createRandomUiNotification(appId),{count:3}); - const livenotifications = faker.helpers.multiple(() => createRandomAppNotification(appId),{count:3}); + const livenotifications = faker.helpers.multiple(() => createRandomAppNotificationWithNoAction(appId),{count:3}); beforeEach(() => { xrmMock = XrmMockGenerator.initialise(); const ctx = xrmMock.Utility.getGlobalContext(); @@ -37,7 +36,6 @@ describe("Given an application ribbon",() =>{ Xrm.App.clearGlobalNotification = vitest.fn(); clearGlobalNotifSpy = vitest.spyOn(Xrm.App,"clearGlobalNotification"); getPublishedNotificationSpy = vitest.spyOn(broadcastNotificationService,"getPublishedNotifications").mockResolvedValue(livenotifications); - }); describe("When it renders notifications on the right page",() =>{ diff --git a/src/broadcast-typescript/tests/services/BroadcastNotificationService.test.ts b/src/broadcast-typescript/tests/services/BroadcastNotificationService.test.ts index 1e6f80a..27cf226 100644 --- a/src/broadcast-typescript/tests/services/BroadcastNotificationService.test.ts +++ b/src/broadcast-typescript/tests/services/BroadcastNotificationService.test.ts @@ -1,7 +1,7 @@ import { fdn_broadcastappnotification } from "@app/modules/domain"; import { broadcastNotificationService, CachedValue } from "@app/modules/services"; import { faker } from "@faker-js/faker"; -import { createRandomAppNotification } from "@tests/fakers"; +import { createRandomAppNotificationWithNoAction } from "@tests/fakers"; import { simpleRetrieveMultipleMock } from "@tests/helpers/mockHelpers"; import { v4 } from "uuid"; import { MockInstance } from "vitest"; @@ -12,14 +12,18 @@ describe("Given a broadcast notification service",() => { describe("When it retrieves published notifications",()=>{ let retrieveMultipleSpy:MockInstance; const notifications:fdn_broadcastappnotification[] = - faker.helpers.multiple(() =>createRandomAppNotification(appId),{count:2}); + faker.helpers.multiple(() =>createRandomAppNotificationWithNoAction(appId),{count:2}); const filterOptions = `&$filter=statuscode eq 2 and fdn_appmoduleid eq '${appId}'&$orderby=fdn_level asc`; const columns = ["fdn_appmoduleid", "fdn_message", "fdn_broadcastappnotificationid", - "fdn_level" + "fdn_level", + "fdn_buttondefaulttext", + "fdn_buttonactionurl", + "fdn_actiontype" ]; - const expandOptions = `&$expand=fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification($select=fdn_contentmessage,fdn_language;$filter=statecode eq 0)`; + const expandColumns = ['fdn_language','fdn_contentmessage','fdn_actionbuttondisplaytext']; + const expandOptions = `&$expand=fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification($select=${expandColumns.join(',')};$filter=statecode eq 0)`; const expectedQueryOptions = `?$select=${columns.join(',')}`+expandOptions+filterOptions; beforeEach(() =>{ retrieveMultipleSpy = simpleRetrieveMultipleMock(notifications); @@ -54,7 +58,7 @@ describe("Given a broadcast notification service",() => { //arrange const cachedItems:CachedValue ={ timestamp: Date.now() - ((60*5) * 1000), - value:faker.helpers.multiple(() =>createRandomAppNotification(appId),{count:1}) + value:faker.helpers.multiple(() =>createRandomAppNotificationWithNoAction(appId),{count:1}) }; sessionStorage.setItem(_cacheKey,JSON.stringify(cachedItems)); //act diff --git a/src/broadcast-typescript/tests/services/XrmAppNotificationFactory.test.ts b/src/broadcast-typescript/tests/services/XrmAppNotificationFactory.test.ts new file mode 100644 index 0000000..02ff7f7 --- /dev/null +++ b/src/broadcast-typescript/tests/services/XrmAppNotificationFactory.test.ts @@ -0,0 +1,83 @@ +import { XrmAppNotificationFactory } from '@app/modules/services/XrmAppNotificationFactory'; +import * as utilities from '@app/modules/common/utility'; +import { MockInstance } from 'vitest'; +import { fdn_broadcastappnotification, fdn_language } from '@app/modules/domain'; +import { createRandomAppNotificationWithNoAction,createRandomAppNotificationWithWebLinkAction } from '@tests/fakers'; +import { v4 } from "uuid"; +vitest.mock('@app/modules/common/utility', () => ( +vitest.importActual('@app/modules/common/utility') +)); +describe('Given a XrmApp Client Notification Factory',() => { + const factory = new XrmAppNotificationFactory(); + const appid = v4(); + let notification:fdn_broadcastappnotification; + let getUserLangFromGlobalContextSpy: MockInstance; + beforeEach(() => { + getUserLangFromGlobalContextSpy = vitest.spyOn(utilities,'getUserLangFromGlobalContext'); + getUserLangFromGlobalContextSpy.mockReturnValue(fdn_language.English); + }); + describe('when creating a client app notification with no action',() => { + let xrmAppNotification:Xrm.App.Notification; + beforeEach(()=> { + notification = createRandomAppNotificationWithNoAction(appid); + xrmAppNotification = factory.create(notification); + }); + it('should not have an action',() => { + expect(xrmAppNotification.action).toBeUndefined(); + }); + it('should have proper level',() => { + const expectedLevel = (notification.fdn_level! - 794560000) + 1; + expect(xrmAppNotification.level).toBe(expectedLevel); + }); + it('should have proper message content',() => { + const expectedMessage = notification.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification!.find((v)=> v.fdn_language === fdn_language.English)!.fdn_contentmessage!; + expect(xrmAppNotification.message).toBe(expectedMessage); + }); + it('should not have a dissmiss button',() => { + expect(xrmAppNotification.showCloseButton).toBe(false); + }); + it('should be a type of message bar',() => { + expect(xrmAppNotification.type).toBe(2); + }); + }); + describe('when creating a client app notification with no localized content',() => { + let xrmAppNotification:Xrm.App.Notification; + beforeEach(()=> { + notification = createRandomAppNotificationWithNoAction(appid,false); + xrmAppNotification = factory.create(notification); + }); + it('should have proper message content',() => { + expect(xrmAppNotification.message).toBe(notification.fdn_message!); + }); + }); + describe('when creating a client app notification with Action',() => { + let xrmAppNotification:Xrm.App.Notification; + beforeEach(()=> { + notification = createRandomAppNotificationWithWebLinkAction(appid); + xrmAppNotification = factory.create(notification); + }); + it('should have proper action text',() => { + const expectedLocalizedObj = notification.fdn_localizednotificationcontent_AppNotificationConfigId_fdn_broadcastappnotification!.find((v)=> v.fdn_language === fdn_language.English); + expect(xrmAppNotification.action!.actionLabel).toBe(expectedLocalizedObj!.fdn_actionbuttondisplaytext!); + }); + it('should have an action event handler',() => { + expect(xrmAppNotification.action!.eventHandler).toBeInstanceOf(Function); + }); + it('should open a web page when action is clicked',() => { + const windowOpenSpy = vitest.spyOn(window,'open').mockReturnValue(null); + expect(xrmAppNotification.action!.eventHandler!()).toBeUndefined(); + expect(windowOpenSpy).toHaveBeenCalledWith(notification.fdn_buttonactionurl!,"_blank"); + }); + }); + describe('when creating a client app notification with Action and no localized content',() => { + let xrmAppNotification:Xrm.App.Notification; + beforeEach(()=> { + notification = createRandomAppNotificationWithWebLinkAction(appid,false); + xrmAppNotification = factory.create(notification); + }); + it('should have proper action text',() => { + expect(xrmAppNotification.action!.actionLabel).toBe(notification.fdn_buttondefaulttext!); + }); + + }); +}); \ No newline at end of file