tag:blogger.com,1999:blog-72583225627213258942024-02-21T16:53:27.275+02:00How To CodeExact answers to exact problems.Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.comBlogger229125tag:blogger.com,1999:blog-7258322562721325894.post-26121120272933520232023-12-14T10:19:00.007+02:002023-12-14T10:21:27.038+02:00SharePoint: How to find out if you have legacy add-ins that are affected by the SharePoint Add-in model retirement<h3 style="text-align: left;">Question</h3><p>How to find out if my SharePoint Online tenant has legacy add-ins that stop working when the legacy add-in model is retiring?</p><p>Answer</p><p>In order to list add-ins, you can use few techniques:</p><p></p><ul style="text-align: left;"><li><a href="https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/get-to-know-the-sharepoint-rest-service" rel="nofollow" target="_blank">SharePoint REST API</a></li><li><a href="https://www.nuget.org/packages/Microsoft.SharePointOnline.CSOM" rel="nofollow" target="_blank">PnP CSOM</a></li><li><a href="https://aka.ms/sppnp-powershell" rel="nofollow" target="_blank">PnP PowerShell</a></li><li><a href="https://pnp.github.io/cli-microsoft365?utm_source=msft_docs&utm_medium=page&utm_campaign=Use+SharePoint+Online+tenant+properties" rel="nofollow" target="_blank">CLI for Microsoft 365</a></li></ul><p></p><p>Easiest in my opinion is the CLI for Microsoft 365, as it will give you the result in just a two commands. If you wish to build further automation, you will want to pick the PnP PowerShell library to use in your PowerShell script or the PnP CSOM if you're using .NET.</p><p>To get a quick list or add-ins using the CLI for Microsoft 365, first login to your tenant with command:</p><p><span style="font-family: courier;">m365 login</span></p><p>then list the add-ins with command:</p><p><span style="font-family: courier;">m365 spo app list</span></p><p>in order to output the add-in JSON array to a file, use command:</p><p><span style="font-family: courier;">m365 spo app list > add-ins.json</span></p>The output will contain array of add-ins, and the property <i>IsClientSideSolution</i> will tell you if the application is legacy and will be retiring. <div><br /></div><div>If <i>IsClientSideSolution</i> is <b>false</b>, it is legacy and will be retired.</div><div><br /></div><div>If <i>IsClientSideSolution</i> is <b>true</b>, it is modern and will NOT be retired.</div><div><br /><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgkMx4p-rU5RZ-rFznmw0fqaTfiW32p8PGE2vgQFWRgpwbe5pF0rMMzllyCqDEOSCKAd-MVnvmHfKdyHFU8GMxoEOlQbu3FZKvypWT3EuLikC7qHM6Yr3wuWnbgFaxQSb0HVIZef3XtE8VetI6qozd7Jsa4FFjtYTdAJauRxAjYjOvgzIf1ij_GnAH4h7Zh" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="433" data-original-width="419" height="240" src="https://blogger.googleusercontent.com/img/a/AVvXsEgkMx4p-rU5RZ-rFznmw0fqaTfiW32p8PGE2vgQFWRgpwbe5pF0rMMzllyCqDEOSCKAd-MVnvmHfKdyHFU8GMxoEOlQbu3FZKvypWT3EuLikC7qHM6Yr3wuWnbgFaxQSb0HVIZef3XtE8VetI6qozd7Jsa4FFjtYTdAJauRxAjYjOvgzIf1ij_GnAH4h7Zh" width="232" /></a></div><br /><br /></div></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-66960575402523167602022-01-12T15:42:00.003+02:002022-01-12T15:45:10.488+02:00Kuluttajariitalautakunnan päätös: Puutteet auton varusteissa<p>Kuluttajariitalautakunta käsitteli tapaukseni kun tammikuussa 2020 ostamastani autosta puuttui varusteita, jotka tilaushetkellä oli varustelistauksessa. Lautakunta päätyi suosittamaan hyvitystä. Hyvitysvaateissanne voitte viitata Kuluttajariitalautakunnan julkiseen päätökseen Dnro 6279/33/2020.</p><p>Päätöksen PDF:n saa Kuluttajariitalautakunnalta tai allekirjoittaneelta pyydettäessä. Alla copy&paste julkisen päätöksen tekstisisällöstä. </p><p><br /></p><p>KULUTTAJARIITALAUTAKUNTA PÄÄTÖS Dnro 6279/33/2020</p><p>Esitelty 28.10.2021</p><p>IVa jaosto </p><p><br /></p><p>Myyjän vastuu auton puuttuvista varusteista</p><p><br /></p><p><b>Lautakunnan ratkaisu </b></p><p>Kuluttajariitalautakunta suosittaa, että Autokeskus Oy maksaa N.Nlle 300 euroa. </p><p><br /></p><p><b>Asiaselostus </b></p><p>Kuluttaja osti 26.10.2019 myyjäliikkeeltä uuden Skoda Superb henkilöauton 53 136,95 </p><p>eurolla. Autolle annettiin kahden vuoden pituinen takuu. Auto toimitettiin kuluttajalle </p><p>29.1.2020.</p><p>Kuluttajan mukaan autosta puuttuu sellaisia varusteita, joiden kuuluisi olla siinä kaupassa </p><p>sovitun mukaisesti. Lisäksi Infotainment-järjestelmä toimii hitaasti. Osapuolet ovat eri mieltä </p><p>siitä, onko kyseessä myyjän vastuulla oleva kaupan kohteen virhe, ja mikä on </p><p>hinnanalennuksen määrä.</p><p><br /></p><p><b>Ostajan vaatimukset perusteluineen</b></p><p>Kuluttaja vaatii myyjäliikkeeltä hyvityksenä 3 000 euroa. Vaatimus perustuu kaupan </p><p>kohteessa ilmenneeseen varusteiden ongelmiin ja siihen sisältyy 2 500 euroa puuttuvista </p><p>varusteista, 400 euroa puutteellisesti toimivista varusteista ja 100 euroa puhelinkuluista ja </p><p>huoltokäynneistä. </p><p>Joitain varusteita puuttuu ja jotkin toimivat puutteellisesti. Kuluttaja totesi virheen keväällä </p><p>2020 ja ilmoitti siitä 9.9.2020. Kuluttaja viittaa vastaukseen ja toteaa, että Personalisointi ei </p><p>toimi eikä ole toiminut syyskuusta 2020 alkaen.</p><p>Kuluttaja on myöhemmin todennut, että mahdollisesti schuko-latausjohdon ongelma saatiin </p><p>poistettua 28.12.2020 eli 11 kuukautta luovutuksesta. Kuluttaja ei pidä järkevinä ratkaisuna </p><p>varusteiden ongelmiin korvata integroidut järjestelmät irrallisilla laitteilla tai manuaalisilla </p><p>toiminnoilla. Muuten ongelmiin ei ole tarjottu ratkaisua. GPS-antenni uusittiin 12.1.2021, </p><p>mikä ei poistanut ongelmia. Infotainment järjestelmälle on lupailtu päivitystä useita kertoja </p><p>turhaan.</p><p>Ratkaisupyynnön liitteenä on ajoneuvon käyttöohjekirja.</p><p><br /></p><p><b>Myyjän vastaus perusteluineen</b></p><p>Myyjäliike ja maahantuoja ovat antaneet asiassa yhteisen vastauksen, ja toteavat voivansa </p><p>maksaa 300 euroa hyvityksenä puuttuvista varusteista.</p><p><br /></p><p>Maahantuojan toimittamaan myyntimateriaaliin oli jäänyt virheellisesti aiemman</p><p>Infotaiment järjestelmän tiedot. Puuttuvia varusteita ovat DVD-soitin 2 SD-korttipaikkaa In </p><p>Car Commonication ja Personalisation 1.1. </p><p><br /></p><p>Autossa on uuden sukupolven Infotainment järjestelmä, jossa on lataushybridijärjestelmälle </p><p>tärkeitä ominaisuuksia kuten akun lataukseen liittyvät toiminnot. Järjestelmässä </p><p>karttapäivitykset tehdään internetyhteydellä. In Car Communication vahvistaa kuljettajan </p><p>ääntä takapenkille ja on tarpeen etenkin seitsenpaikkaisessa autossa. Personalisointi 1.1 </p><p>tallentaa kuljettajan istuimen säädöt ym. avaimeen. Tässä autossa tallennus voidaan tehdä </p><p>istuimen muistipaikkapainikkeisiin ja se on ollut käytettävissä Connect-palvelun kautta </p><p>viimeistään syyskuusta 2020 alkaen.</p><p><br /></p><p>Mode 2 -latausjohtoa varten on olemassa ohjelmistopäivitys veloituksetta. Sen jälkeen </p><p>voidaan käyttää täyttä 8A latausvirtaa (n. 1,8 kW). Ennen päivitystä latausvirta oli enintään </p><p>6A ja muut lataustavat olivat normaalisti käytettävissä. Asiakkaalle on lähetetty tästä tieto </p><p>3.12.2020 ja 11.12.2020. Mahdollisten Infotainment-järjestelmän ongelmien vuoksi asiakasta</p><p>on pyydetty tuomaan auto huoltoon tarkistettavaksi 16.9. ja 3.12.2020 sekä myös </p><p>11.12.2020. GPS-antenni on uusittu, koska Connect-yhteys oli pätkinyt. Infotainment järjestelmälle on tulossa päivitys viikkoon 29 mennessä vuonna 2021.</p><p>Vastauksen liitteenä on Autokeskuksen työmääräyksiä. </p><p><br /></p><p><b>Ratkaisun perustelut</b></p><p>Virheellisyyden arviointi </p><p>Virheen arvioinnin lähtökohta on osapuolten välisen sopimuksen sisältö. Uuden auton </p><p>kaupassa virheen olemassa oloa arvioidaan kuluttajansuojalain 5 luvun 12 §:n mukaan. </p><p>Virhearvioinnin perustana ovat ostajan aiheelliset odotukset.</p><p>Asiassa on riidatonta, että autosta on puuttunut siihen kuuluneita varusteita. Kuluttaja on </p><p>perustellusti voinut edellyttää, että autossa on hänelle ilmoitetut varusteet, joten kaupassa </p><p>on tällä perusteella kuluttajansuojalain 5 luvun 12 §:n mukainen virhe. Kauppa ei ole kaikilta </p><p>osin vastannut sitä, mistä asiassa on sovittu.</p><p><br /></p><p>Infotainment-järjestelmän osalta autoon on tehty päivityksiä, ja tältä osin asiassa ei ole </p><p>tarkempaa tietoa siitä, mikä on järjestelmän tilanne päivitysten jälkeen. Asiassa ei ole </p><p>esitetty ulkopuolista selvitystä siitä, voidaanko kuluttajan esiin nostama ongelma katsoa </p><p>kuluttajansuojalain mukaiseksi virheeksi. Tältä osin asiassa ei esitetyn näytön valossa ole </p><p>todettavissa virhettä.</p><p><br /></p><p><b>Virheen seuraamukset</b></p><p>Myyjäliike saa omalla kustannuksellaan korjata virheen, jos se tarjoutuu tekemään</p><p>korjauksen viipymättä saatuaan tietää virheestä. Myyjällä on oikeus osoittaa ostajalle myyjän</p><p>lukuun tehtävän korjauksen paikka. Ostaja saa kieltäytyä virheen korjaamisesta vain</p><p>erityisestä syystä, esimerkiksi, jos siitä aiheutuisi hänelle olennaista haittaa. </p><p>Tässä tapauksessa virheen korjaaminen ei ole mahdollista, koska ajoneuvoon ei ole</p><p>asennettu puuttuvia varusteita, vaan ne on tarjottu käytettäviksi ulkoisina järjestelminä, joita</p><p>ei ole integroitu autoon. Näin ollen tarjottu korjaustapa ei ole asianmukainen, ja asiaa on</p><p>arvioitava hinnanalennukseen perustuen.</p><p><br /></p><p>Hyvityksen määrän arviointi perustuu ilmenneen virheen laadun ja merkityksen ohella</p><p>kauppahintaan sekä ostajan aiheellisiin odotuksiin.</p><p>Nämä seikat huomioon otettuna lautakunta katsoo, että myyjäliikkeen tulee suorittaa</p><p>ostajalle hinnanalennuksena ja korvauksena tarpeellisista kuluista yhteensä 300 euroa.</p><p>Päätös oli yksimielinen.</p>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-84096091655587137922021-11-02T10:26:00.006+02:002021-11-02T10:26:41.524+02:00SharePoint: Transport-level error has occurred when receiving results from the server<h3 style="text-align: left;">Problem</h3><p>SharePoint 2016 (on-prem) farm had weird issues on few application and WFE servers. Namely, the servers couldn't connect to SQL Server, but ULS logs showed error:</p><p><span style="font-family: courier;">Unknown SQL Exception 64 occurred. Additional error information from SQL Server is included below. A transport-level error has occurred when receiving results from the server. (provider: TCP Provider, error: 0 - The specified network name is no longer available.)</span></p><p>When testing the connection with UDL file, the behavior was also strange, i.e., when testing the connection to server without defining any specific database, the test succeeded:<br /><br /></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQbduS7IQIgxQAOB0LCF8eTjyKpR7j0nKXs-eTuMvlxMisH_VIvHdw4ojeQWJHYmj3F6ys6X0Wnr2d5wxdMTfl-5n7cf-eW88ZvkzPJvc3IaqvSpR033lTmqHD_po_9rJhyphenhyphenPzynfux8nqz/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="463" data-original-width="361" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQbduS7IQIgxQAOB0LCF8eTjyKpR7j0nKXs-eTuMvlxMisH_VIvHdw4ojeQWJHYmj3F6ys6X0Wnr2d5wxdMTfl-5n7cf-eW88ZvkzPJvc3IaqvSpR033lTmqHD_po_9rJhyphenhyphenPzynfux8nqz/w312-h400/image.png" width="312" /></a></div><br />Also when defining a specific database, the test succeeded:<br /><br /><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRWt1BfqGXoWvxpDs7IhO2qXDy34PoMrtmbHBfYg595g4Om2CL0atWUux3sGTR7rIO1B3PHdVns6Tmd8XnEI_rFbTriK5Hpe627OXdVxvhPvYkeJGYDrp6Gw0UD5buKVwaLXAR-PMsP2E6/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="469" data-original-width="390" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRWt1BfqGXoWvxpDs7IhO2qXDy34PoMrtmbHBfYg595g4Om2CL0atWUux3sGTR7rIO1B3PHdVns6Tmd8XnEI_rFbTriK5Hpe627OXdVxvhPvYkeJGYDrp6Gw0UD5buKVwaLXAR-PMsP2E6/w333-h400/image.png" width="333" /></a></div><div class="separator" style="clear: both; text-align: left;"><br /></div><div class="separator" style="clear: both; text-align: left;">However, when attempting to list the databases by expanding the "Select the database on the server" dropdown, there was first "Unspesified error" error:</div><div class="separator" style="clear: both; text-align: left;"><br /></div><div class="separator" style="clear: both; text-align: left;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5B1LmCSigZJAm57Ub6I6SPLocn90HSkgG1G5BA9hewllmr84db8VFn7K6xwJOd1Cvq_PoMKSKnL2-GRBAFTqmcildqam9cZCoWK2DpeMpvcaBwQZrsFIAVSzCzO-C4aiaxjQn3GKpQJfy/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="463" data-original-width="361" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5B1LmCSigZJAm57Ub6I6SPLocn90HSkgG1G5BA9hewllmr84db8VFn7K6xwJOd1Cvq_PoMKSKnL2-GRBAFTqmcildqam9cZCoWK2DpeMpvcaBwQZrsFIAVSzCzO-C4aiaxjQn3GKpQJfy/w312-h400/image.png" width="312" /></a></div><br />Followed by error "Microsoft Data Link <br />Login failed. Catalog information cannot be retrieved.":</div><p></p><p></p><div class="separator" style="clear: both; text-align: left;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihP3u0IH9XbGSoHA-VFOiRKR1WKV3ehkjD2ItNtIupM912EvOun1fTAed573cvkw1_w3Wm8QEaT6ImePd0RMv9EoKXugExaSm9iXQ_VehG_UiuRllNeRvV0ANuzFckF5mDmwXzCLxhaA0y/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="480" data-original-width="429" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihP3u0IH9XbGSoHA-VFOiRKR1WKV3ehkjD2ItNtIupM912EvOun1fTAed573cvkw1_w3Wm8QEaT6ImePd0RMv9EoKXugExaSm9iXQ_VehG_UiuRllNeRvV0ANuzFckF5mDmwXzCLxhaA0y/w358-h400/image.png" width="358" /></a></div><br /><h3 style="text-align: left;">Solution</h3><div>Solution was (eventually) to fix the Jumbo Packet setting on the servers having the issue. Other servers had value 1514, while the problematic servers had value 9014 and changing value to 1514 made all the SharePoint servers immediately connect to SQL Server, and also the UDL DB listing started working.</div></div><p></p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIbU9dWuwDJ2vZDVWAhIdYRFn5PuV7Pp6po6D8jj3gE0nHwjCjyFsgTcRVAejPxXAFYoxTVpCrsgklraid7W4N4Us-BslBGUnVmqNEVqkOlazV-LqOOhoKXAws7SKBB5hT8_hIACLphefi/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="524" data-original-width="569" height="368" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIbU9dWuwDJ2vZDVWAhIdYRFn5PuV7Pp6po6D8jj3gE0nHwjCjyFsgTcRVAejPxXAFYoxTVpCrsgklraid7W4N4Us-BslBGUnVmqNEVqkOlazV-LqOOhoKXAws7SKBB5hT8_hIACLphefi/w400-h368/image.png" width="400" /></a></div><br /><br /><p></p>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-46419955116363428732021-09-29T09:52:00.007+03:002024-01-16T14:39:45.662+02:00Edge: Reverting to classic authentication dialog a.k.a disable Windows Hello for HTTP authentication<h3 style="text-align: left;">Problem</h3><p>In recent Microsoft Edge browser versions 90+, the classic authentication dialog (or NTLM authentication dialog, or Windows authentication prompt) has been replaced by Windows Hello authentication prompt. It's all nice and secure, but at the moment at least, browser password vault extensions such as 1Password cannot fill in the credentials to that modern prompt. What it means is that you need to close the Windows Hello prompt, open password extension, copy username/password to notepad, refresh browser window, paste credentials from notepad to Windows Hello prompt. *yawn*</p><p>This is cumbersome in enterprise scenarios with various internal systems such as SharePoint that may require you to login with different credentials from the one you're currently logged into Windows.</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgb9IxaS-a4KNWrGb8jk1kIB3QOqIeQ1Dr5Kzfunqh9xYMtlhUtfirDslYAyZecxCANOpS2ksTV5I_5lkGlpPSPkanmrsYxsizj1Y9lKS63GNJfO6xLZqrOr84aa2laqBTgCcFiyIQNCFrD/" style="margin-left: 1em; margin-right: 1em;"><img data-original-height="488" data-original-width="747" height="261" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgb9IxaS-a4KNWrGb8jk1kIB3QOqIeQ1Dr5Kzfunqh9xYMtlhUtfirDslYAyZecxCANOpS2ksTV5I_5lkGlpPSPkanmrsYxsizj1Y9lKS63GNJfO6xLZqrOr84aa2laqBTgCcFiyIQNCFrD/w400-h261/image.png" width="400" /></a></div><br /><br /><p></p><h3 style="text-align: left;">Solution</h3><p>For now the only solution is to disable the Windows Hello prompt in Edge. It will require using Group Policies either on AD level, or on individual machine. The following steps are for individual machine, but if you're an AD admin, you can pick the essential pieces from the instructions and do the same on AD level policy.</p><p></p><ol style="text-align: left;"><li>First download MS Edge policy file from <a href="https://aka.ms/EdgeEnterprise" target="_blank">https://aka.ms/EdgeEnterprise</a>, from the drop-downs, select the version of your Edge, then press <b>GET POLICY FILES</b><br /><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7CvewMeWwmvDLZ-4iibqjJ9y_h0RE8Ma7WMEl1RDV2fRtBNkbfRx8YF9mKaw25j9T8AIVRs4NqKfKKyUb7EgnY__aqEnbfn805iQBY4zldju_wf7ZdTaqZzj3xQtNC1KV10lzzeN1pm7J/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="270" data-original-width="1258" height="86" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7CvewMeWwmvDLZ-4iibqjJ9y_h0RE8Ma7WMEl1RDV2fRtBNkbfRx8YF9mKaw25j9T8AIVRs4NqKfKKyUb7EgnY__aqEnbfn805iQBY4zldju_wf7ZdTaqZzj3xQtNC1KV10lzzeN1pm7J/w400-h86/image.png" width="400" /></a></div><br /></li><li>Extract the .cab, and .zip 🙄</li><li>Navigate to <i>.\MicrosoftEdgePolicyTemplates\windows\admx</i> folder</li><li>Copy <i>msedge.admx</i> to <i>C:\Windows\PolicyDefinitions</i></li><li>Navigate to <i>.\MicrosoftEdgePolicyTemplates\windows\admx\en-US</i> folder (NOTE! or the language of your Windows installation, if not en-US)</li><li>Copy <i>msedge.adml</i> to <i>C:\Windows\PolicyDefinitions\en-US</i></li><li>Open Local Group Policy Editor, and navigate to <i>Computer Configuration / Administrative Templates / Microsoft Edge / HTTP Authentication</i></li><li>Edit <i>Windows Hello For HTTP Auth Enabled </i>setting, and set it to <i>Disabled<br /><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjgD30X3_QNembfrGLX1I5c-4HuJmr9WpCXUYp-zbmW82GTYUDwA6mOfbg2kdtw0WGD6cJ56LYN8HOe7J7bO1ik8ZpPH_Q7qYO7Q9tq1Pj8A_daTxEdV09ejZfZzupH63fyQpC2sqU63bcn/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="560" data-original-width="1017" height="220" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjgD30X3_QNembfrGLX1I5c-4HuJmr9WpCXUYp-zbmW82GTYUDwA6mOfbg2kdtw0WGD6cJ56LYN8HOe7J7bO1ik8ZpPH_Q7qYO7Q9tq1Pj8A_daTxEdV09ejZfZzupH63fyQpC2sqU63bcn/w400-h220/image.png" width="400" /></a></div><br /></i></li><li>Click OK to confirm policy setting, and refresh page in Edge - no restart needed</li><li>Applauds! Classic authentication prompt is back and you can also access the browser extension<br /><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEisODexZhzmjtxFzgsplej1NdaKCisUV79U9Bmk-Ox5awXV4Ev0FV8wj7whYdVei9PqwGSB9RUMwMILLI9wxZPRVQ-y8JfCrs6x5EWxhBSCNqbnG7N_W-SdKVg5fTGgUYUwC0b9oIBy00Ei/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="268" data-original-width="715" height="150" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEisODexZhzmjtxFzgsplej1NdaKCisUV79U9Bmk-Ox5awXV4Ev0FV8wj7whYdVei9PqwGSB9RUMwMILLI9wxZPRVQ-y8JfCrs6x5EWxhBSCNqbnG7N_W-SdKVg5fTGgUYUwC0b9oIBy00Ei/w400-h150/image.png" width="400" /></a></div></li></ol>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-50851721887206594122021-09-20T10:43:00.005+03:002021-09-21T11:05:02.367+03:00Auth0: Invalid RSAES-OAEP padding<h3 style="text-align: left;">Problem</h3><p>After configuring Auth0 with custom certificates via API, you get Access Denied error when attempting to login.</p><p><span style="color: #2b2e2f; font-size: 14px;"><span style="font-family: courier;">{ "error": "access_denied", "error_description": "Invalid RSAES-OAEP padding." }</span></span></p><p><br /></p><h3 style="text-align: left;">Solution</h3><p>Add an additional <span face=""Lucida Sans Unicode", sans-serif" style="color: #2b2e2f; font-size: 10.5pt;"> </span><code style="white-space: pre-wrap;"><span style="background: rgb(248, 248, 248); border: 1pt solid rgb(234, 234, 234); color: #2b2e2f; font-family: Consolas; font-size: 10pt; mso-border-alt: solid #EAEAEA .75pt; padding: 0cm;">decryptionKey</span></code><span face=""Lucida Sans Unicode", sans-serif" style="color: #2b2e2f; font-size: 10.5pt;"> </span>to the connection's options with the following format.</p>
<div style="background: rgb(248, 248, 248); border: 1pt solid rgb(204, 204, 204); mso-border-alt: solid #CCCCCC .75pt; mso-element: para-border-div; padding: 5pt 8pt;"><pre style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; border-radius: 3px; border: none; line-height: 14.25pt; max-width: 100%; padding: 0cm;"><code style="border: transparent; max-width: 100%; white-space: pre-wrap;"><span style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #2b2e2f; font-family: Consolas;">options: {<o:p></o:p></span></code></pre><pre style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; border: none; line-height: 14.25pt; padding: 0cm;"><code><span style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #2b2e2f; font-family: Consolas;"> //... other options<o:p></o:p></span></code></pre><pre style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; border: none; line-height: 14.25pt; padding: 0cm;"><code><span style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #2b2e2f; font-family: Consolas;"> "decryptionKey" : {</span></code></pre><pre style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; border: none; line-height: 14.25pt; padding: 0cm;"><code><span style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #2b2e2f; font-family: Consolas;"><span><span> </span> "key": </span>"-----BEGIN PRIVATE KEY-----\n...",<o:p></o:p></span></code></pre><pre style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; border: none; line-height: 14.25pt; padding: 0cm;"><code><span style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #2b2e2f; font-family: Consolas;"> <span> </span>"cert": "-----BEGIN CERTIFICATE-----\n..."</span></code></pre><pre style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; border: none; line-height: 14.25pt; padding: 0cm;"><code><span style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #2b2e2f; font-family: Consolas;"><span> </span>}</span></code></pre><pre style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; border: none; line-height: 14.25pt; padding: 0cm;"><code><span style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #2b2e2f; font-family: Consolas;">}<o:p></o:p></span></code></pre></div>
<span face=""Lucida Sans Unicode",sans-serif" style="color: #2b2e2f; font-size: 10.5pt; mso-ansi-language: FI; mso-bidi-language: AR-SA; mso-fareast-font-family: Calibri; mso-fareast-language: FI; mso-fareast-theme-font: minor-latin;"><div><span face=""Lucida Sans Unicode",sans-serif" style="color: #2b2e2f; font-size: 10.5pt; mso-ansi-language: FI; mso-bidi-language: AR-SA; mso-fareast-font-family: Calibri; mso-fareast-language: FI; mso-fareast-theme-font: minor-latin;"><br /></span></div></span>Keep in mind that options are replaced, not merged - so you'll need to send the whole options object to the PATCH call.Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-39719163713809305322021-04-28T14:22:00.002+03:002021-04-28T14:23:43.207+03:00Azure B2C: Adding missing translations on Page Layouts<h3 style="text-align: left;">Problem</h3><p>After starting to use Azure B2C custom <a href="https://docs.microsoft.com/en-us/azure/active-directory-b2c/page-layout" target="_blank">Page Layout versions newer than 2.0.0</a>, you will find translations are missing on many controls. Documentation is lacking behind, so it will take some trial and error to figure out some of the translation IDs. In the following pictures, you see missing translations marked with beautiful hand drawn red arrows.</p><p></p><div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2LJAgW7j-6oWHPpFperQzqRGePf_Jmc4tvp0UPLxq2PVOzIQuTFZmHq0eacCOh8HsWVmf7YE3WF3GnovlFrMTme7YOme1bk0TqswATCMQ-MgaKg9HwVXkoZ6Z1VaA_-YM-Gj9ildcdcoH/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="410" data-original-width="446" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2LJAgW7j-6oWHPpFperQzqRGePf_Jmc4tvp0UPLxq2PVOzIQuTFZmHq0eacCOh8HsWVmf7YE3WF3GnovlFrMTme7YOme1bk0TqswATCMQ-MgaKg9HwVXkoZ6Z1VaA_-YM-Gj9ildcdcoH/s16000/image.png" /></a></div><br /><br /></div><div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-WsIN45oBiJpLtqQoaMoUAgLO-LNrPlUwFE80ij_jlVI736H82jLEb_EIx0LR2u7b9nPOBg3giP3XbJgj9K8yUD9eN0p3x640LG7ks-Re_Mn7mOL0brLeiXO3tyKqT3S_GxeNc9suHIcC/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="475" data-original-width="458" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-WsIN45oBiJpLtqQoaMoUAgLO-LNrPlUwFE80ij_jlVI736H82jLEb_EIx0LR2u7b9nPOBg3giP3XbJgj9K8yUD9eN0p3x640LG7ks-Re_Mn7mOL0brLeiXO3tyKqT3S_GxeNc9suHIcC/s16000/image.png" /></a></div></div><p></p><h3 style="text-align: left;">Solution</h3><p>As B2C _should_ already include these translations, the only workaround currently is to manually provide the missing strings in your Custom Policy. The following will work for <i>urn:com:microsoft:aad:b2c:elements:contract:unifiedssp:2.1.4</i> and <i>urn:com:microsoft:aad:b2c:elements:contract:selfasserted:2.1.4</i>.</p><p>So first of all, add <i>LocalizedResourceReference</i> elements in the <i>ContentDefinition</i> elements.</p><!--HTML generated using hilite.me--><div style="background: rgb(255, 255, 255); border-color: gray; border-image: initial; border-style: solid; border-width: 0.1em 0.1em 0.1em 0.8em; border: solid gray; overflow: auto; padding: 0.2em 0.6em; width: auto;"><table><tbody><tr><td><pre style="line-height: 125%; margin: 0px;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22</pre></td><td><pre style="line-height: 125%; margin: 0px;"><span style="color: #007700;"><ContentDefinition</span> <span style="color: #0000cc;">Id=</span><span style="background-color: #fff0f0;">"api.signuporsignin"</span><span style="color: #007700;">></span>
<span style="color: #007700;"><LoadUri></span>https://xyz.blob.core.windows.net/customui/ocean_blue/unified.html<span style="color: #007700;"></LoadUri></span>
<span style="color: #007700;"><RecoveryUri></span>https://xyz.blob.core.windows.net/customui/ocean_blue/exception.html<span style="color: #007700;"></RecoveryUri></span>
<span style="color: #007700;"><DataUri></span>urn:com:microsoft:aad:b2c:elements:contract:unifiedssp:2.1.4<span style="color: #007700;"></DataUri></span>
<span style="color: #007700;"><Metadata></span>
<span style="color: #007700;"><Item</span> <span style="color: #0000cc;">Key=</span><span style="background-color: #fff0f0;">"DisplayName"</span><span style="color: #007700;">></span>Signin and Signup<span style="color: #007700;"></Item></span>
<span style="color: #007700;"></Metadata></span>
<span style="color: #007700;"><LocalizedResourcesReferences</span> <span style="color: #0000cc;">MergeBehavior=</span><span style="background-color: #fff0f0;">"Prepend"</span><span style="color: #007700;">></span>
<span style="color: #007700;"><LocalizedResourcesReference</span> <span style="color: #0000cc;">Language=</span><span style="background-color: #fff0f0;">"fi"</span> <span style="color: #0000cc;">LocalizedResourcesReferenceId=</span><span style="background-color: #fff0f0;">"api.signuporsignin.fi"</span> <span style="color: #007700;">/></span>
<span style="color: #007700;"></LocalizedResourcesReferences></span>
<span style="color: #007700;"></ContentDefinition></span>
<span style="color: #007700;"><ContentDefinition</span> <span style="color: #0000cc;">Id=</span><span style="background-color: #fff0f0;">"api.selfasserted"</span><span style="color: #007700;">></span>
<span style="color: #007700;"><LoadUri></span>https://xyz.blob.core.windows.net/customui/ocean_blue/selfAsserted.html<span style="color: #007700;"></LoadUri></span>
<span style="color: #007700;"><RecoveryUri></span>https://xyz.blob.core.windows.net/customui/ocean_blue/exception.html<span style="color: #007700;"></RecoveryUri></span>
<span style="color: #007700;"><DataUri></span>urn:com:microsoft:aad:b2c:elements:contract:selfasserted:2.1.4<span style="color: #007700;"></DataUri></span>
<span style="color: #007700;"><Metadata></span>
<span style="color: #007700;"><Item</span> <span style="color: #0000cc;">Key=</span><span style="background-color: #fff0f0;">"DisplayName"</span><span style="color: #007700;">></span>Collect information from user page<span style="color: #007700;"></Item></span>
<span style="color: #007700;"></Metadata></span>
<span style="color: #007700;"><LocalizedResourcesReferences</span> <span style="color: #0000cc;">MergeBehavior=</span><span style="background-color: #fff0f0;">"Prepend"</span><span style="color: #007700;">></span>
<span style="color: #007700;"><LocalizedResourcesReference</span> <span style="color: #0000cc;">Language=</span><span style="background-color: #fff0f0;">"fi"</span> <span style="color: #0000cc;">LocalizedResourcesReferenceId=</span><span style="background-color: #fff0f0;">"api.localaccountpasswordreset.fi"</span> <span style="color: #007700;">/></span>
<span style="color: #007700;"></LocalizedResourcesReferences></span>
<span style="color: #007700;"></ContentDefinition></span>
</pre></td></tr></tbody></table></div>
<p>Then the actual strings you will add in the <i>Localization</i> element.</p><!--HTML generated using hilite.me--><div style="background: rgb(255, 255, 255); border-color: gray; border-image: initial; border-style: solid; border-width: 0.1em 0.1em 0.1em 0.8em; border: solid gray; overflow: auto; padding: 0.2em 0.6em; width: auto;"><table><tbody><tr><td><pre style="line-height: 125%; margin: 0px;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22</pre></td><td><pre style="line-height: 125%; margin: 0px;"><span style="color: #007700;"><LocalizedResources</span> <span style="color: #0000cc;">Id=</span><span style="background-color: #fff0f0;">"api.signuporsignin.fi"</span><span style="color: #007700;">></span>
<span style="color: #007700;"><LocalizedStrings></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"ClaimType"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"signInName"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"DisplayName"</span><span style="color: #007700;">></span>Sähköpostiosoite<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"ClaimType"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"password"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"DisplayName"</span><span style="color: #007700;">></span>Salasana<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"UxElement"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"local_intro_generic"</span><span style="color: #007700;">></span>Kirjaudu sisään aiemmin luodulla tililläsi<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"></LocalizedStrings></span>
<span style="color: #007700;"></LocalizedResources></span>
<span style="color: #007700;"><LocalizedResources</span> <span style="color: #0000cc;">Id=</span><span style="background-color: #fff0f0;">"api.localaccountpasswordreset.fi"</span><span style="color: #007700;">></span>
<span style="color: #007700;"><LocalizedStrings></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"ClaimType"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"email"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"DisplayName"</span><span style="color: #007700;">></span>Sähköpostiosoite<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"ClaimType"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"VerificationCode"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"DisplayName"</span><span style="color: #007700;">></span>Vahvistuskoodi<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"ClaimType"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"signInNames.emailAddress"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"DisplayName"</span><span style="color: #007700;">></span>Sähköpostiosoite<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"DisplayControl"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"emailVerificationSSPRControl"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"email"</span><span style="color: #007700;">></span>Sähköpostiosoite<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"DisplayControl"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"emailVerificationSSPRControl"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"ver_input"</span><span style="color: #007700;">></span>Vahvistuskoodi<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"DisplayControl"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"emailVerificationSSPRControl"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"verificationcode"</span><span style="color: #007700;">></span>Vahvistuskoodi<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"DisplayControl"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"emailVerificationSSPRControl"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"intro_msg"</span><span style="color: #007700;">></span>Syötä sähköpostiosoitteesi ja paina Lähetä vahvistuskoodi -painiketta.<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"DisplayControl"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"emailVerificationSSPRControl"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"but_send_code"</span><span style="color: #007700;">></span>Lähetä vahvistuskoodi<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"DisplayControl"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"emailVerificationSSPRControl"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"but_verify_code"</span><span style="color: #007700;">></span>Vahvista koodi<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"DisplayControl"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"emailVerificationSSPRControl"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"but_send_new_code"</span><span style="color: #007700;">></span>Lähetä uusi koodi<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"><LocalizedString</span> <span style="color: #0000cc;">ElementType=</span><span style="background-color: #fff0f0;">"DisplayControl"</span> <span style="color: #0000cc;">ElementId=</span><span style="background-color: #fff0f0;">"emailVerificationSSPRControl"</span> <span style="color: #0000cc;">StringId=</span><span style="background-color: #fff0f0;">"success_send_code_msg"</span><span style="color: #007700;">></span>Vahvistuskoodi on lähetetty sähköpostiisi. Kopioi se alla olevaan syöteruutuun ja paina Vahvista koodi -painiketta.<span style="color: #007700;"></LocalizedString></span>
<span style="color: #007700;"></LocalizedStrings></span>
<span style="color: #007700;"></LocalizedResources></span>
</pre></td></tr></tbody></table></div>
<p>Please note that this is not a comprehensive list of missing translations, so feel free to comment below if you happen to have a full tested list of translations that will work with the new Page Layouts.</p>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com2tag:blogger.com,1999:blog-7258322562721325894.post-82130041463271547882021-03-23T13:31:00.004+02:002021-03-23T13:32:35.727+02:00Office Add-In: Empty group label<h3 style="text-align: left;">Problem</h3><p>I needed to create Outlook Add-In Ribbon button without Group label, like the Insights Add-In does.</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3N28b57Cnma7hlUU5WMoNlF5Qdcm0LRtuBPb3lwwlip-mtIqTlULi1p7M_E8FpaBNv8rnu8486KhYmI8zEZPyzXWUSTCqz1FLvwKMEDyoekHTHZTczwr8A4xTrlEi8LEi4UWidqm2xgFl/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="99" data-original-width="213" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3N28b57Cnma7hlUU5WMoNlF5Qdcm0LRtuBPb3lwwlip-mtIqTlULi1p7M_E8FpaBNv8rnu8486KhYmI8zEZPyzXWUSTCqz1FLvwKMEDyoekHTHZTczwr8A4xTrlEi8LEi4UWidqm2xgFl/s16000/image.png" /></a></div><br /><br /><p></p><h3 style="text-align: left;">Solution</h3><p>As the <i>Group</i> element requires <i>Label</i>, and the String of the Label requires <i>DefaultValue</i> to have some value, the workaround was to set the <i>DefaultValue</i> as one space character.</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhV0IJlfRVIZ5cocWSMUGwupzVuqSS8gtFW4yK-BtCKO1zcgfdvG5zZhNRukBsRojfgOflHGE-PeYgCYcGDs1HhW8QyfVxt42vthNKOKsAaspqbznGBTHYLphUPrsOYlsZIRh-HIH4uc9GD/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="93" data-original-width="553" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhV0IJlfRVIZ5cocWSMUGwupzVuqSS8gtFW4yK-BtCKO1zcgfdvG5zZhNRukBsRojfgOflHGE-PeYgCYcGDs1HhW8QyfVxt42vthNKOKsAaspqbznGBTHYLphUPrsOYlsZIRh-HIH4uc9GD/s16000/image.png" /></a></div><br /><div class="separator" style="clear: both; text-align: center;"><br /></div><div style="clear: both; text-align: center;"><span style="font-size: x-large;"><b>Ta-daa!<br /></b></span></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgf3aW71C3l86lSUXOQV-L7mH9ldRHrGp_2pFZzWYUh5QvSF9uEW-13dkAvaCzBBL9wH8vkPGTnEat_G-ipZJwpAt8gLoZ5_5YJysmjYFXSY6c_BZnKVFjN7BimN1jdJyOojWeDDFb6UFwO/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="109" data-original-width="290" height="120" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgf3aW71C3l86lSUXOQV-L7mH9ldRHrGp_2pFZzWYUh5QvSF9uEW-13dkAvaCzBBL9wH8vkPGTnEat_G-ipZJwpAt8gLoZ5_5YJysmjYFXSY6c_BZnKVFjN7BimN1jdJyOojWeDDFb6UFwO/" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><br /></div><br /><br /><p></p>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-73207042956972117742021-02-26T09:58:00.026+02:002021-03-18T14:53:22.627+02:00WebView2: How to hide scrollbars<h3 style="text-align: left;">Problem</h3><p>When using the new <a href="https://docs.microsoft.com/en-us/microsoft-edge/webview2/" target="_blank">Microsoft Edge WebView2</a> control, it often displays scroll bars and the control doesn't have any explicit property to hide the scroll bars.</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhg_yiOIyTbGEX0NioCtGqJXIUvb2aUDxVuu8e0ysbkahHHNRLp7Lz63xF50Q8s_HcgHFXeYuJhJFF38p2shWTID6WpNFmfy4FuyXNkAB6vna0nfDwydIWDB5So2Dzzr3Z7VCqptGLKRxPI/" style="margin-left: 1em; margin-right: 1em;"><img data-original-height="399" data-original-width="649" height="246" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhg_yiOIyTbGEX0NioCtGqJXIUvb2aUDxVuu8e0ysbkahHHNRLp7Lz63xF50Q8s_HcgHFXeYuJhJFF38p2shWTID6WpNFmfy4FuyXNkAB6vna0nfDwydIWDB5So2Dzzr3Z7VCqptGLKRxPI/w400-h246/image.png" width="400" /></a></div><br /><br /><p></p><h3 style="text-align: left;">Solution</h3><p>You need to use custom Javascript at the <b>NavigationCompleted </b>event to hide the scrollbars. </p><p>Simply add a new <b>NavigationCompleted </b>event handler for the WebView2 control and use the <b>ExecuteScriptAsync </b>method to run a Javascript that hides the scroll bars.</p><!--HTML generated using hilite.me--><div style="background: rgb(255, 255, 255); border-color: gray; border-image: initial; border-style: solid; border-width: 0.1em 0.1em 0.1em 0.8em; border: solid gray; overflow: auto; padding: 0.2em 0.6em; width: auto;"><table><tbody><tr><td><pre style="line-height: 125%; margin: 0px;">1
2
3
4
5
6
7</pre></td><td><pre style="line-height: 125%; margin: 0px;"><span style="color: #008800; font-weight: bold;">private</span> <span style="color: #008800; font-weight: bold;">void</span> <span style="color: #0066bb; font-weight: bold;">WebView2_NavigationCompleted</span>(<span style="color: #333399; font-weight: bold;">object</span> sender, CoreWebView2NavigationCompletedEventArgs e)
{
<span style="color: #008800; font-weight: bold;">if</span> (e.IsSuccess)
{
((WebView2)sender).ExecuteScriptAsync(<span style="background-color: #fff0f0;">"document.querySelector('body').style.overflow='hidden'"</span>);
}
}
</pre></td></tr></tbody></table></div><div><br /></div><div><b>Note!</b> Code above hides scrollbars AND disables scrolling. If you would like to hide scrollbars but retain scrolling (with touch for example), please use this code instead.</div><div><br /></div><!--HTML generated using hilite.me--><div style="background: rgb(255, 255, 255); border-color: gray; border-image: initial; border-style: solid; border-width: 0.1em 0.1em 0.1em 0.8em; border: solid gray; overflow: auto; padding: 0.2em 0.6em; width: auto;"><table><tbody><tr><td><pre style="line-height: 125%; margin: 0px;">1
2
3
4
5
6
7</pre></td><td><pre style="line-height: 125%; margin: 0px;"><span style="color: #008800; font-weight: bold;">private</span> <span style="color: #008800; font-weight: bold;">void</span> <span style="color: #0066bb; font-weight: bold;">WebView2_NavigationCompleted</span>(<span style="color: #333399; font-weight: bold;">object</span> sender, CoreWebView2NavigationCompletedEventArgs e)
{
<span style="color: #008800; font-weight: bold;">if</span> (e.IsSuccess)
{
((WebView2)sender).ExecuteScriptAsync(<span style="background-color: #fff0f0;">"document.querySelector('body').style.overflow='scroll';var style=document.createElement('style');style.type='text/css';style.innerHTML='::-webkit-scrollbar{display:none}';document.getElementsByTagName('body')[0].appendChild(style)"</span>);
}
}
</pre></td></tr></tbody></table></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-80132119495001932822021-01-27T15:01:00.004+02:002021-01-27T15:07:06.296+02:00Azure Managed Identity: Obtaining token gives error ‘invalid_client’<h2 style="text-align: left;">Problem</h2><p>One of our Azure App Services suddenly started behaving badly and throwing HTTP 400 errors. From Application Insights we could see the error was coming from a call to LOCALHOST:PORT/MSI/token which is the location where access token is requested in case your code wants to access other Azure resources using Managed Identity (formerly MSI).</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgna0UTePemnIdLyEONU7Y0bQNB19ttakFEK1uJj1F67tjkSsPZaR2YQtjSpRajXqydxAWihYnkhsviaxdqreJgDWzkQDkDE1uXkLS1MOUbfJll1WHQz5rFs5YiMTrOuebCeh-kx6mtljhN/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="477" data-original-width="514" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgna0UTePemnIdLyEONU7Y0bQNB19ttakFEK1uJj1F67tjkSsPZaR2YQtjSpRajXqydxAWihYnkhsviaxdqreJgDWzkQDkDE1uXkLS1MOUbfJll1WHQz5rFs5YiMTrOuebCeh-kx6mtljhN/s16000/image.png" /></a></div><h2 style="text-align: left;">Troubleshooting</h2><p><span style="font-family: inherit;">I went to Kudu PowerShell console of the given App Service and tried to manually get the <em>access_token</em>, but couldn’t. </span></p><p>Command for that is: <br /><span style="font-family: courier;">Invoke-WebRequest -Uri 'http://127.0.0.1:41332/MSI/token/?resource=https://management.azure.com/&api-version=2017-09-01' -Method GET -Headers @{Metadata="true";Secret="$env:MSI_SECRET"} -UseBasicParsing</span> <br /><br /><strong>Note! </strong>Port in the URL is different in your App Service, you can get it via <span style="font-family: courier;">@env:MSI_ENDPOINT.</span></p><p>All I got was <em>HTTP 401</em> error with <em>‘invalid_client’</em> error code. Strange. In respective DEV App Service there was no errors and <em>access_code</em> was returned nicely.</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMeSUaL7T2Bi0MNIP2Gadx9dJA93hQKAzJRdJl-dOy7ki42xXK9xuSMLVtpaKtmCP9uB5KGSGvtfysAFmJISKdHeO4gxYlnMpPPdk1jpFsyiyVHHd9Oyy2l29z3BiRaLuEQp0iFuTXvL5A/" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="240" data-original-width="588" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMeSUaL7T2Bi0MNIP2Gadx9dJA93hQKAzJRdJl-dOy7ki42xXK9xuSMLVtpaKtmCP9uB5KGSGvtfysAFmJISKdHeO4gxYlnMpPPdk1jpFsyiyVHHd9Oyy2l29z3BiRaLuEQp0iFuTXvL5A/s16000/image.png" /></a></div><p>By the way, details of the Uri and other parameters can be found <a href="https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=powershell#obtain-tokens-for-azure-resources" target="_blank">here</a>. Header is different if you’re using more recent <em>api-version</em>. </p><p>By the way, if you just run the Invoke-WebRequest, you will get error:</p><p><em>Win32 internal error "The handle is invalid" 0x6 occurred while reading the console output buffer. Contact Microsoft Customer Support Services.</em></p><p>No point in contacting MS Support, just run the following command and retry:</p><p><span style="font-family: courier;">$ProgressPreference="SilentlyContinue"</span></p><h2 style="text-align: left;">Solution</h2><p>Now, for the solution…good old <strong><span style="font-size: x-large;">IISRESET</span></strong>. Of course in Azure you restart the App Service in question. After restarting the App Service, you can re-run the <em>Invoke-WebRequest</em>, and access_token is returned correctly, and App Service works.</p>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-20641863758634207642020-06-26T13:15:00.001+03:002020-06-26T13:33:11.211+03:00MS Flow: Simplest retry logic for SharePoint Online HTTP 400 errors<h3>Problem</h3> <p>When setting SharePoint Online document properties from Flow, you will run into issues if the document is locked, i.e., someone has it open. In this case, SharePoint throws HTTP 400 that cannot be caught by the built-in retry-logic of the <em>Update Item</em> Flow action.</p> <h3>Solution</h3> <p>Simplest Do Until loop I came up with can be seen below. I didn’t find using <em>Scope</em> action necessary. In case you need to re-use this elsewhere in your Flow, it is quite straight forward to copy the <em>Do Until </em>action and paste it elsewhere. Just remember to add <em>Set Variable</em> action before each Do Until and set the <strong>fileLocked</strong> variable to <strong>true</strong>.</p> <p>At first, the two <em>Set variable </em>actions were a bit confusing, the first one is only set to run after the SharePoint Update Item action has succeeded (and in that you set the fileLocked to false). The second one, however, is set to be run if the previous <em>Set variable</em> action is skipped (and in that you set the fileLocked to true), and as the first one is skipped if the Update Item fails, we then know it did NOT succeed.</p> <p>It feels a bit weird to have the second<em> Set variable</em> (Set variable 2) as fileLocked variable value is not changing, but this is the high level logic people seem to do this so there may be some room for further improvement.</p> <a href="https://drive.google.com/uc?id=1TOY0S49yXE8-eE9Ey38QjoBFqVS5lodw"><img title="flowretry" style="margin-right: auto; margin-left: auto; float: none; display: block; background-image: none;" border="0" alt="flowretry" src="https://drive.google.com/uc?id=15_ryUjNWz25zPlMcedBony7DILYcsZ1T" width="536" height="893" /></a>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com1tag:blogger.com,1999:blog-7258322562721325894.post-82772749637983742202020-05-14T12:07:00.003+03:002020-05-14T12:10:47.852+03:00Microsoft Flow: Using HTTP Webhook action with Azure Automation Runbook<h3>Task</h3><p>I needed to create new SharePoint Online Document Library and amongst other things set a Retention Label on that newly created Document Library using Microsoft Flow. Creating new doclib is straightforward using Flow, but I just couldn’t set the the Retention Label via REST from Flow. There is an API for that, but due to reasons I couldn’t get it to work from Flow. </p><p>We had an Azure Automation Runbook that was called at the end of the Flow anyway, so I decided to use that to set the Label on the SPO Library using SharePoint Online PnP’s <em>Set-PnPLabel.</em> No problem. However, it takes a while for the Label to be applied to the Library and as email was sent to users at the end of the Flow, they found themselves in the library too early, i.e., the Label was not yet set.</p><h3>Solution</h3><h5>FLOW</h5><p>I could’ve used a “Do Until” loop in Flow and poll the list but that’s not something we like to do, right? We like events and triggers, so why not use the <em>HTTP Webhook</em> action, sounds exciting!</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsESj3qJnpn9UsW0cJAu4p3DFjJCxZGRuFk3nuTAJMyocRnBWpphbqaoH7PMaJev9ZE56CF5mR8WC5CaKEk7M5xdoPWwSFcN6Wb_ukGxfD7kbd6Ad2ssohr9yeryf5ez62Vna6KPPoJaTA/" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="400" data-original-width="607" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsESj3qJnpn9UsW0cJAu4p3DFjJCxZGRuFk3nuTAJMyocRnBWpphbqaoH7PMaJev9ZE56CF5mR8WC5CaKEk7M5xdoPWwSFcN6Wb_ukGxfD7kbd6Ad2ssohr9yeryf5ez62Vna6KPPoJaTA/d/wh1.png" /></a></div><p><br /></p><p>In the screenshot above, I’m calling the Azure Automation Runbook, but you can really call anything that is capable of listening to your request and at the and making a HTTP request back to the callback URL you define. You define your endpoint address in the <em>Subscribe - URI </em>field.</p><p>In the <em>Subscribe - Body</em> you must at least pass in the <em>listCallbackUrl()</em> so that you know the endpoint of this specific Flow instance you need to call in your backend code. Note that <em>listCallbackUrl()</em> is generated automatically, is specific to this running instance of the Flow, and <strong>can only be called once</strong>. Flow processing will halt at this action and it will continue when your backend code calls the <em>listCallbackUrl().</em> You can define timeout of how long the action waits for the callback in the action settings.</p><p>Here I’m also passing in the title of the SharePoint Library as I’m also doing some tricks on the library but you would pass in anything you need in your scenario.</p><h5>BACKEND</h5><p>In the backend code you will do whatever you need, but when you’re done, make HTTP POST call to the URL you received as a parameter in your backend code, in my case that would look like this in PowerShell.</p><!--HTML generated using hilite.me--><div style="background: rgb(255, 255, 255); border-color: gray; border-image: none 100% / 1 / 0 stretch; border-style: solid; border-width: 0.1em 0.1em 0.1em 0.8em; overflow: auto; padding: 0.2em 0.6em; width: auto;"><table><tbody><tr><td><pre style="line-height: 125%; margin: 0px;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17</pre></td><td><pre style="line-height: 125%; margin: 0px;"><span style="color: #008800; font-weight: bold;">param</span>(
[<span style="color: #008800; font-weight: bold;">Parameter</span> (<span style="color: #008800; font-weight: bold;">Mandatory</span> = <span style="color: #996633;">$true</span>)]
<span style="color: #003366; font-weight: bold;">[object]</span><span style="color: #996633;">$webhookData</span>
)
<span style="color: #888888;"># If runbook was called from Webhook, WebhookData will not be null.</span>
<span style="color: #008800; font-weight: bold;">if</span> (<span style="color: #996633;">$WebhookData</span>) {
<span style="color: #888888;"># Retrieve VMs from Webhook request body</span>
<span style="color: #996633;">$body</span> = (<span style="color: #007020;">ConvertFrom-Json</span> -InputObject <span style="color: #996633;">$WebhookData</span>.RequestBody)
<span style="color: #888888;">####</span>
<span style="color: #888888;"># Do something in your code...</span>
<span style="color: #888888;">###</span>
<span style="color: #888888;"># ...and when you're done, call the callback URL</span>
<span style="color: #007020;">Invoke-WebRequest</span> <span style="color: #996633;">$body</span>.CallbackUrl -Method POST -UseBasicParsing
}
</pre></td></tr></tbody></table></div><p>If you look at the Flow when it is running, you see it pause at the <em>HTTP Webhook</em> action, and continue as soon as the backend calls the callback URL. You can also manually call the callback URL using e.g., Postman, just paste in the callback URL and make sure method is POST. You will get HTTP 200 when the callback call succeeds.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEju9_qmEkG3PxG93nycOj6nONJ3gmMMKM9qkN5yt7tc-CJmvCLQ9oKheyt8j2HNGQ1Tl4Yiq5uradSllpjQG1CRbvv9ZcRJnlPEQOqB3UbWIIlmyCeb1MPtdxXzkNB29Ioy7F4C1yrholqE/" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="443" data-original-width="990" height="286" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEju9_qmEkG3PxG93nycOj6nONJ3gmMMKM9qkN5yt7tc-CJmvCLQ9oKheyt8j2HNGQ1Tl4Yiq5uradSllpjQG1CRbvv9ZcRJnlPEQOqB3UbWIIlmyCeb1MPtdxXzkNB29Ioy7F4C1yrholqE/w640-h286/wh2.png" width="640" /></a></div><p><br /></p>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-23463702208199498022020-04-14T12:12:00.001+03:002020-04-14T12:23:55.164+03:00Cloning OneNote Tab without /clone REST endpoint<h3>Series</h3><p>This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the <a href="https://www.nuget.org/packages/Microsoft.Graph/3.1.0" target="_blank">Microsoft.Graph (3.1.0).</a> First post discusses things in general, details about different tab types are separated to individual articles.</p><ol><li><a href="https://blog.jussipalo.com/2020/04/cloning-teams-channels-and-tabs-without.html" target="_blank">Cloning Teams Channels and Tabs without /clone REST endpoint</a></li><li><a href="https://blog.jussipalo.com/2020/04/cloning-planner-tab-without-clone-rest.html" target="_blank">Cloning Planner Tab without /clone REST endpoint</a></li><li><strong>Cloning <em>OneNote</em> Tab without /clone REST endpoint <<YOU ARE HERE>></strong></li><li>Cloning <em>Web Tab</em> without /clone REST endpoint (TBD)</li><li><a href="https://blog.jussipalo.com/2020/04/copying-teams-files-tab-content-using.html" target="_blank">Copying Teams Files tab content using MoveCopyUtil</a></li></ol><h3>Solution</h3><p>In this piece of code, we first determine current source tab is of type OneNote. We must first create new Notebook for the tab, and for this we add simple retry logic as you cannot have Notebooks with duplicate name. This retry logic adds running integer to Notebook name until the creation succeeds.</p><p>Now, after we have successfully created the Notebook, it is time to create the Tab. Do note the special format of the EntityId parameter for the tab, as well as rather identical URLs.</p><p>Finally, note that you may end up getting ServiceException although tab creation succeeds, thus the scary Exception swallowing.</p><p>Using term <em>cloning</em> when it comes to this tab type can be a bit misleading, as we’re not cloning the tab content, but only the tab and creating new content. Perhaps think of this as <em>cloning</em> from the Team perspective, while the individual tabs </p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(0, 136, 0); font-weight: bold;">if</span> (sourceTab.TeamsApp.Id.Equals(TeamsAppId.OneNote))
{
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Creating OneNote. Channel: {sourceChannel.DisplayName}. {newTeamId}"</span>);
<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> newNotebook = <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
var i = <span style="color: rgb(102, 0, 238); font-weight: bold;">0</span>;
Notebook nb = <span style="color: rgb(0, 136, 0); font-weight: bold;">null</span>;
<span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> nbName;
<span style="color: rgb(0, 136, 0); font-weight: bold;">while</span> (i < <span style="color: rgb(102, 0, 238); font-weight: bold;">100</span> && <span style="color: rgb(0, 136, 0); font-weight: bold;">null</span> == nb)
{
nbName = <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"{TextTools.RemoveSpecialCharactersForNotebook(requestData.title)}{(i > 0 ? $"</span> {i}<span style="background-color: rgb(255, 240, 240);">" : "")} Notebook"</span>;
<span style="color: rgb(0, 136, 0); font-weight: bold;">try</span>
{
nb = <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> graphClient.Groups[newGroup.Id].Onenote.Notebooks.Request().AddAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> Notebook()
{
DisplayName = nbName
});
<span style="color: rgb(0, 136, 0); font-weight: bold;">break</span>;
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">catch</span> (ServiceException ex)
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">if</span> ((<span style="color: rgb(51, 51, 153); font-weight: bold;">int</span>)ex.StatusCode == <span style="color: rgb(102, 0, 238); font-weight: bold;">409</span>)
{
<span style="color: rgb(136, 136, 136);">// this is fine, Notebook with such name already exists, let's find first available by appending integers to name</span>
logger?.LogWarning(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Notebook with name '{nbName}' already exists, trying next integer. Channel: {sourceChannel.DisplayName}. {newTeamId}"</span>);
}
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">finally</span>
{
i++;
}
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">return</span> nb;
});
<span style="color: rgb(0, 136, 0); font-weight: bold;">try</span>
{
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Adding OneNote tab. Channel: {sourceChannel.DisplayName}. {newTeamId}"</span>);
<span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> graphClient.Teams[newTeamId].Channels[channelId].Tabs.Request().AddAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> TeamsTab()
{
ODataType = <span style="color: rgb(0, 136, 0); font-weight: bold;">null</span>,
DisplayName = newNotebook.DisplayName,
Configuration = <span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> TeamsTabConfiguration()
{
EntityId = <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"{Guid.NewGuid()}_{newNotebook.Id}"</span>,
ContentUrl = <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"https://www.onenote.com/teams/TabContent?entityid=%7BentityId%7D&subentityid=%7BsubEntityId%7D&auth_upn=%7Bupn%7D&notebookSource=New&notebookSelfUrl=https%3A%2F%2Fwww.onenote.com%2Fapi%2Fv1.0%2FmyOrganization%2Fgroups%2F{{groupId}}%2Fnotes%2Fnotebooks%2F{newNotebook.Id}&oneNoteWebUrl={newNotebook.Links.OneNoteWebUrl.Href}&notebookName={newNotebook.DisplayName}&ui={{locale}}&tenantId={{tid}}"</span>,
RemoveUrl = <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"https://www.onenote.com/teams/TabRemove?entityid=%7BentityId%7D&subentityid=%7BsubEntityId%7D&auth_upn=%7Bupn%7D&notebookSource=New&notebookSelfUrl=https%3A%2F%2Fwww.onenote.com%2Fapi%2Fv1.0%2FmyOrganization%2Fgroups%2F{{groupId}}%2Fnotes%2Fnotebooks%2F{newNotebook.Id}&oneNoteWebUrl={newNotebook.Links.OneNoteWebUrl.Href}&notebookName={newNotebook.DisplayName}&ui={{locale}}&tenantId={{tid}}"</span>,
WebsiteUrl = <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"https://www.onenote.com/teams/TabRedirect?redirectUrl={newNotebook.Links.OneNoteWebUrl.Href}"</span>
},
AdditionalData = <span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> Dictionary<<span style="color: rgb(51, 51, 153); font-weight: bold;">string</span>, <span style="color: rgb(51, 51, 153); font-weight: bold;">object</span>>()
{
{
<span style="background-color: rgb(255, 240, 240);">"teamsApp@odata.bind"</span>, <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{sourceTab.TeamsApp.Id}"</span>
}
}
});
});
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">catch</span> (ServiceException ex)
{
HandleTabCreationException(ex, logger, sourceTab, newTeamId);
}
}
</pre></td></tr></tbody></table></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-91697676625050294682020-04-09T13:38:00.001+03:002020-04-14T12:24:14.396+03:00Cloning Planner Tab without /clone REST endpoint<h3>Series</h3><p>This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the <a href="https://www.nuget.org/packages/Microsoft.Graph/3.1.0" target="_blank">Microsoft.Graph (3.1.0).</a> First post discusses things in general, details about different tab types are separated to individual articles.</p><ol><li><a href="https://blog.jussipalo.com/2020/04/cloning-teams-channels-and-tabs-without.html" target="_blank">Cloning Teams Channels and Tabs without /clone REST endpoint</a></li><li><strong>Cloning <em>Planner</em> Tab without /clone REST endpoint <<YOU ARE HERE>></strong></li><li><a href="https://blog.jussipalo.com/2020/04/cloning-onenote-tab-without-clone-rest.html" target="_blank">Cloning OneNote Tab without /clone REST endpoint</a></li><li>Cloning <em>Web Tab</em> without /clone REST endpoint (TBD)</li><li><a href="https://blog.jussipalo.com/2020/04/copying-teams-files-tab-content-using.html" target="_blank">Copying Teams Files tab content using MoveCopyUtil</a></li></ol><h3>Solution</h3><p>In this piece of code, we first determine current source tab is of type Planner. Then comes the tricky part: you cannot use Application permissions to create the Planner Plan itself, but you must use Delegated permissions, so basically what you need is a dedicated user account, and use a <em>GraphServiceClient</em> that uses <em>UsernamePasswordProvider </em>as<em> IAuthenticationProvider </em>for the Graph API calls. Yes, you really gotta put username and password of that user in Key Vault and fetch those here.</p><p>Now, after we have successfully created the Planner Plan, we will do a small trick and do not create the <em>contentUrl</em> property for the new tab manually, but take the <em>ContentUrl</em> from the source Tab and just replace the old Planner Id with the Id of the newly created Planner. Lovely. Just note that the <em>WebsiteUrl</em> needs to be done separately, otherwise the “Go to website” link on top right of the Planner tab is broken and will be stuck loading and give error <em>“Error loading user settings. Please try again. If this continues please contact customer support.”</em> after few minutes.</p><p>For creating the new Tab, there’s not much to say. These are the parameters you need to define based on a bit of trial and error. As Graph API evolves and improves, this is the part that will most probably need updating, but at least with Microsoft.Graph v. 3.1.0 it works – which is already old version as 3.2.0 was released few hours ago.</p><p>Finally, note that you may end up getting ServiceException although tab creation succeeds, thus the scary Exception swallowing.</p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(136, 136, 136);">// Planner</span>
<span style="color: rgb(0, 136, 0); font-weight: bold;">if</span> (sourceTab.TeamsApp.Id.Equals(TeamsAppId.Planner))
{
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Creating Planner. Channel: {sourceChannel.DisplayName}. {newGroup.Id}"</span>);
<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> newPlannerPlan = <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">return</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> Authentication.GetGraphApiClientClientUsernamePasswordProvider(keyvault).Planner.Plans.Request()
.WithUsernamePassword(keyvault.GetSecret(<span style="background-color: rgb(255, 240, 240);">"teamsClone--SPOUser"</span>), <span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> System.Net.NetworkCredential(<span style="background-color: rgb(255, 240, 240);">""</span>, keyvault.GetSecret(<span style="background-color: rgb(255, 240, 240);">"teamsClone--SPOPassword"</span>)).SecurePassword)
.AddAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> PlannerPlan()
{
Owner = newGroup.Id,
Title = <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"{TextTools.RemoveSpecialCharactersForNotebook(requestData.title)} Planner"</span>
});
});
<span style="color: rgb(0, 136, 0); font-weight: bold;">try</span>
{
<span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> contentUrl = sourceTab.Configuration.ContentUrl.Replace(sourceTab.Configuration.EntityId, newPlannerPlan.Id);
<span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> websiteUrl = <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"https://tasks.office.com/YOURTENANT.onmicrosoft.com/Home/Planner#/plantaskboard?groupId={newTeamId}&planId={newPlannerPlan.Id}"</span>;
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Adding Planner tab. Channel: {sourceChannel.DisplayName}, ContentUrl: {contentUrl}. {newGroup.Id}"</span>);
<span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> graphClient.Teams[newTeamId].Channels[channelId].Tabs.Request().AddAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> TeamsTab()
{
ODataType = <span style="color: rgb(0, 136, 0); font-weight: bold;">null</span>,
DisplayName = sourceTab.DisplayName,
TeamsApp = sourceTab.TeamsApp,
Configuration = <span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> TeamsTabConfiguration()
{
EntityId = newPlannerPlan.Id,
ContentUrl = contentUrl,
RemoveUrl = contentUrl,
WebsiteUrl = webSiteUrl
},
AdditionalData = <span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> Dictionary<<span style="color: rgb(51, 51, 153); font-weight: bold;">string</span>, <span style="color: rgb(51, 51, 153); font-weight: bold;">object</span>>()
{
{
<span style="background-color: rgb(255, 240, 240);">"teamsApp@odata.bind"</span>, <span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{sourceTab.TeamsApp.Id}"</span>
}
}
});
});
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">catch</span> (ServiceException ex)
{
HandleTabCreationException(ex, logger, sourceTab);
}
}
</pre></td></tr></tbody></table></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-72332356802869109092020-04-09T12:54:00.001+03:002020-04-14T12:28:01.193+03:00Cloning Teams Channels and Tabs without /clone REST endpoint<h3>Series</h3><p>This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the <a href="https://www.nuget.org/packages/Microsoft.Graph/3.1.0" target="_blank">Microsoft.Graph (3.1.0).</a> First post discusses things in general, details about different tab types are separated to individual articles.</p><ol><li><strong>Cloning Teams Channels and Tabs without /clone REST endpoint <<YOU ARE HERE>></strong></li><li><a href="https://blog.jussipalo.com/2020/04/cloning-planner-tab-without-clone-rest.html" target="_blank">Cloning Planner Tab without /clone REST endpoint</a></li><li><a href="https://blog.jussipalo.com/2020/04/cloning-onenote-tab-without-clone-rest.html" target="_blank">Cloning OneNote Tab without /clone REST endpoint</a></li><li>Cloning <em>Web Tab</em> without /clone REST endpoint (TBD)</li><li><a href="https://blog.jussipalo.com/2020/04/copying-teams-files-tab-content-using.html" target="_blank">Copying Teams Files tab content using MoveCopyUtil</a></li></ol><h3>Task</h3><p>As cloning a Microsoft Teams Team using the <font face="Courier New">https://graph.microsoft.com/v1.0/teams/{sourceeamId}/clone</font> has few things that might surprise you (such as described <a href="https://laurakokkarinen.com/cloning-teams-and-configuring-tabs-via-microsoft-graph-cloning-a-team/" target="_blank">here</a>), you will anyway need go through all cloned tabs to verify they’re working. Why not instead create a fresh Channel based on the source Team Channel, then enumerate source Team tabs and recreate new tabs in the destination Channel based on the properties of the source Tab?</p><h3>Solution</h3><p>Enough chit-chat, lets dive in! </p><p>What happens in the code is that we first get source Team, Channels, and their Tabs. Having already created new Group and related Team (out of scope of this specific article), we start creating Channels in the new Team based on source Team Channels. </p><p>Reason why I didn’t include details of creating new Group and related Team here is because you could apply this same logic on existing Teams as well. Maybe if you’d like to push new Channel or Tab to existing Teams? Welcome, this code will work just nice.</p><p>After creating each Channel, we loop through the source Channel Tabs. Based on the source Tab type, and also how it is eventually been configured, we recreate identical Tab.</p><p>Good thing in this way of doing this is that you have full control of the Tab cloning operation.Then again it also means that you will have to implement that full control logic and program code.</p><p>Depending on the tab type, term <em>cloning</em> might be a bit misleading, as in all cases we’re not cloning tab content but creating new background resource (such as Planner Plan or OneNote Notebook) and just binding to it from the tab. Maybe think of cloning here as cloning the Team <em>structure </em>including channels and tabs.</p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41</pre></td><td><pre style="margin: 0px; line-height: 125%;">logger?.LogInformation(<span style="background-color: rgb(255, 240, 240);">"Getting source team channels and tabs"</span>);
<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> sourceTeamChannels = <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">return</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> graphClient.Teams[sourceTeamId].Channels.Request().GetAsync();
});
<span style="color: rgb(0, 136, 0); font-weight: bold;">foreach</span> (<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> chan <span style="color: rgb(0, 136, 0); font-weight: bold;">in</span> sourceTeamChannels)
{
chan.Tabs = <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">return</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> graphClient.Teams[sourceTeamId].Channels[chan.Id].Tabs.Request().Expand(<span style="background-color: rgb(255, 240, 240);">"TeamsApp"</span>).GetAsync();
});
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">foreach</span> (<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> sourceChannel <span style="color: rgb(0, 136, 0); font-weight: bold;">in</span> sourceTeamChannels)
{
<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> newChannel = <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Adding channel {sourceChannel.DisplayName}. {newGroup.Id}"</span>);
<span style="color: rgb(0, 136, 0); font-weight: bold;">return</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> graphClient.Teams[newTeamId].Channels.Request().AddAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> Microsoft.Graph.Channel()
{
ODataType = <span style="color: rgb(0, 136, 0); font-weight: bold;">null</span>,
DisplayName = sourceChannel.DisplayName,
Description = sourceChannel.Description
});
});
channelId = newChannel.Id;
<span style="color: rgb(136, 136, 136);">// here you should remove the Wiki tab (see separate article)</span>
<span style="color: rgb(0, 136, 0); font-weight: bold;">foreach</span> (<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> sourceTab <span style="color: rgb(0, 136, 0); font-weight: bold;">in</span> sourceChannel.Tabs)
{
<span style="color: rgb(136, 136, 136);">// here you will have switch or if/else construct to determine source tab type</span>
<span style="color: rgb(0, 136, 0); font-weight: bold;">if</span> (sourceTab.TeamsApp.Id.Equals(TeamsAppId.<span style="color: rgb(136, 136, 136);">/*TAB TYPE*/</span>))
{
<span style="color: rgb(136, 136, 136);">// and actual tab type specific creation and configuration logic</span>
}
}
}
</pre></td></tr></tbody></table></div><p>By the way, you’ll be needing this Exception handler later, as tab creation will throw specific <em>ServiceExceptions</em> even though creation succeeds.</p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;"> 1
2
3
4
5
6
7
8
9
10
11
12</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(0, 136, 0); font-weight: bold;">private</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">static</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">void</span> <span style="color: rgb(0, 102, 187); font-weight: bold;">HandleTabCreationException</span>(ServiceException ex, ILogger logger, TeamsTab sourceTab)
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">if</span> (ex.StatusCode.ToString() == <span style="background-color: rgb(255, 240, 240);">"BadRequest"</span> && ex.Error.Message == <span style="background-color: rgb(255, 240, 240);">"Value cannot be null.\r\nParameter name: entity"</span>)
{
<span style="color: rgb(136, 136, 136);">// this is fine: https://stackoverflow.com/questions/59784200/programmatically-creating-teamtab-used-to-work-but-now-gives-error-after-updati</span>
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Tab {sourceTab.TeamsApp.Id} created: {sourceTab.DisplayName}"</span>);
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">else</span>
{
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Error creating tab: {sourceTab.TeamsApp.Id}: {sourceTab.DisplayName}"</span>);
}
}
</pre></td></tr></tbody></table></div><p>Also, extending Laura’s <a href="https://laurakokkarinen.com/cloning-teams-and-configuring-tabs-via-microsoft-graph-configuring-the-sharepoint-and-files-tabs/" target="_blank">TeamsAppId class</a> a bit will come handy, thank you Laura!</p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;">1
2
3
4
5
6
7
8</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(0, 136, 0); font-weight: bold;">public</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">static</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">class</span> <span style="color: rgb(187, 0, 102); font-weight: bold;">TeamsAppId</span>
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">public</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">static</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">readonly</span> <span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> OneNote = <span style="background-color: rgb(255, 240, 240);">"0d820ecd-def2-4297-adad-78056cde7c78"</span>;
<span style="color: rgb(0, 136, 0); font-weight: bold;">public</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">static</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">readonly</span> <span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> Planner = <span style="background-color: rgb(255, 240, 240);">"com.microsoft.teamspace.tab.planner"</span>;
<span style="color: rgb(0, 136, 0); font-weight: bold;">public</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">static</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">readonly</span> <span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> SharePoint = <span style="background-color: rgb(255, 240, 240);">"2a527703-1f6f-4559-a332-d8a7d288cd88"</span>;
<span style="color: rgb(0, 136, 0); font-weight: bold;">public</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">static</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">readonly</span> <span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> Web = <span style="background-color: rgb(255, 240, 240);">"com.microsoft.teamspace.tab.web"</span>;
<span style="color: rgb(0, 136, 0); font-weight: bold;">public</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">static</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">readonly</span> <span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> Wiki = <span style="background-color: rgb(255, 240, 240);">"com.microsoft.teamspace.tab.wiki"</span>;
}
</pre></td></tr></tbody></table></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-61816150087941863412020-04-08T12:55:00.001+03:002020-04-14T12:15:06.847+03:00Copying Teams Files tab content using MoveCopyUtil<h3>Series</h3><p>This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the <a href="https://www.nuget.org/packages/Microsoft.Graph/3.1.0" target="_blank">Microsoft.Graph (3.1.0).</a> First post discusses things in general, details about different tab types are separated to individual articles.</p><ol><li><a href="https://blog.jussipalo.com/2020/04/cloning-teams-channels-and-tabs-without.html" target="_blank">Cloning Teams Channels and Tabs without /clone REST endpoint</a></li><li><a href="https://blog.jussipalo.com/2020/04/cloning-planner-tab-without-clone-rest.html" target="_blank">Cloning Planner Tab without /clone REST endpoint</a></li><li><a href="https://blog.jussipalo.com/2020/04/cloning-onenote-tab-without-clone-rest.html" target="_blank">Cloning OneNote Tab without /clone REST endpoint</a></li><li>Cloning <em>Web Tab</em> without /clone REST endpoint (TBD)</li><li><strong>Copying Teams Files tab content using MoveCopyUtil <<YOU ARE HERE>></strong></li></ol><h3>Task</h3><p>As part of Teams provisioning, content of Files tab needed to be copied from source Team to the destination Team. As you might know, in practice it just means copying folders and files across SharePoint site collections.</p><h3>Solution</h3><p><em>Microsoft.SharePoint.Client.MoveCopyUtil</em> will do the trick UNLESS a folder name contains special characters, see <a href="https://github.com/SharePoint/sp-dev-docs/issues/5261" target="_blank">here</a> for example. This has been an issue for long in CSOM, and if at all possible, I recommend not having special characters in folder names, and not in file names either for that matter. </p><p>If you must support special character scenarios, it is doable, but will require carefully splitting and constructing the query parameters and effectively means you need to loop and create/copy folders and files one by one. I’ve spent <em>some</em> time creating SPFx solution that manages to do this, and I recommend thinking not twice, but ten times before agreeing to support special characters in this case. Good thing is that in this Teams provisioning scenario you will have control over the source Team and should be able to work around any needs for special characters.</p><h3>Explanation of the code</h3><p>What happens in this code can probably be read quite well by looking at the <em>LogInformation</em> entries, but the idea is that we get hold of the Drive item of the source and destination Group. <em>Drive.WebUrl</em> is then absolute URL to the document library where Teams File tab contents are in their own sub-folders.</p><p>As a reminder, each File tab in a Team Channel maps to one root level folder (Channel name = folder name) in that document library you find at <em>Drive.WebUrl</em>.</p><p>Another nice thing about <em>Group.Drive</em> is that whenever we get hold of that, and it is not returning HTTP 404, we know Group provisioning (the process that runs in the background and creates all back-end dependencies, such as the SharePoint site) has been completed.</p><p><strong>retry </strong>object is <a href="https://github.com/App-vNext/Polly" target="_blank">Polly</a>’s <em>AsyncRetryPolicy</em> and ensures retry logic in case provisioning would not be yet ready at this point. It will retry the call as configured, in my case every 5 seconds, until the .GetAsync() completes successfully.</p><p>Next up there is some ugly parsing of the source document library path, namely we’re removing the document library part from the URL, leaving us with URL of the <em>SPWeb</em>. There are many ways of doing this, and I admit this one is quite verbose way of doing it but doesn’t at least require additional HTTP query.</p><p>Then we get to the actual point, initializing SharePoint context using the source <em>SPWeb</em>, and credentials stored in Azure Key Vault.</p><p>After getting all the root level folders (each corresponding a Channel, yes, good), we loop through each folder item and use <em>MoveCopyUtil.CopyFolder</em> to copy it to destination document library. Just remember to set the <em>MoveCopyOptions</em> to <em>KeepBoth</em>, indicating this is a <strong>Copy</strong> operation and not <strong>Move</strong> operation.</p><p>I felt more comfortable doing <em>.ExecuteQuery</em> for each folder, although it means few more HTTP calls, but allows improved error handling in case of those special characters, or some other problems that might occur during the copy operation.</p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(0, 136, 0); font-weight: bold;">public</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">static</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> Task <span style="color: rgb(0, 102, 187); font-weight: bold;">CopyDocuments</span>(<span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> srcGroupId, <span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> dstGroupId, AsyncRetryPolicy retry, GraphServiceClient graphClient, KeyVault keyvault, ILogger logger)
{
<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> sourceDrive = <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">return</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> graphClient.Groups[srcGroupId].Drive.Request().GetAsync();
});
<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> destDrive = <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> retry.ExecuteAsync(<span style="color: rgb(0, 136, 0); font-weight: bold;">async</span> () =>
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">return</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">await</span> graphClient.Groups[dstGroupId].Drive.Request().GetAsync();
});
<span style="color: rgb(0, 136, 0); font-weight: bold;">try</span>
{
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Copying SPO files, source drive: {sourceDrive.WebUrl}"</span>);
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Copying SPO files, destination drive: {destDrive.WebUrl}"</span>);
<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> sourceUri = <span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> Uri(sourceDrive.WebUrl);
<span style="color: rgb(51, 51, 153); font-weight: bold;">string</span> sourceSiteUrlWithoutLastSegment = sourceUri.AbsoluteUri.Remove(sourceUri.AbsoluteUri.Length - sourceUri.Segments.Last().Length);
<span style="color: rgb(0, 136, 0); font-weight: bold;">using</span> (<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> srcContext = <span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> OfficeDevPnP.Core.AuthenticationManager()
.GetSharePointOnlineAuthenticatedContextTenant(sourceSiteUrlWithoutLastSegment, keyvault.GetSecret(<span style="background-color: rgb(255, 240, 240);">"teamsClone--SPOUser"</span>), keyvault.GetSecret(<span style="background-color: rgb(255, 240, 240);">"teamsClone--SPOPassword"</span>)))
{
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Copying SPO files, getting root folders"</span>);
<span style="color: rgb(136, 136, 136);">// get folders at root </span>
<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> targetList = srcContext.Web.Lists.GetByTitle(sourceDrive.Name);
<span style="color: rgb(136, 136, 136);">// This method only gets the folders which are on top level of the list/library</span>
FolderCollection oFolderCollection = targetList.RootFolder.Folders;
<span style="color: rgb(136, 136, 136);">// Load folder collection</span>
srcContext.Load(oFolderCollection);
srcContext.ExecuteQuery();
MoveCopyOptions option = <span style="color: rgb(0, 136, 0); font-weight: bold;">new</span> MoveCopyOptions
{
KeepBoth = <span style="color: rgb(0, 136, 0); font-weight: bold;">true</span>
};
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Copying SPO files, looping folders"</span>);
<span style="color: rgb(0, 136, 0); font-weight: bold;">foreach</span> (<span style="color: rgb(51, 51, 153); font-weight: bold;">var</span> folder <span style="color: rgb(0, 136, 0); font-weight: bold;">in</span> oFolderCollection)
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">if</span> (folder.Name.Equals(<span style="background-color: rgb(255, 240, 240);">"Forms"</span>))
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">continue</span>;
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">try</span>
{
logger?.LogInformation(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Copying SPO files, copying folder '{sourceUri.GetLeftPart(UriPartial.Authority) + folder.ServerRelativeUrl}' -> '{destDrive.WebUrl}/{folder.Name}'"</span>);
MoveCopyUtil.CopyFolder(
srcContext,
sourceUri.GetLeftPart(UriPartial.Authority) + folder.ServerRelativeUrl,
<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"{destDrive.WebUrl}/{folder.Name}"</span>,
option);
srcContext.ExecuteQuery();
logger?.LogInformation(<span style="background-color: rgb(255, 240, 240);">"Copying done"</span>);
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">catch</span> (Exception ex)
{
logger?.LogError(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Error copying SPO folder {folder.Name}. {ex.Message}"</span>);
}
}
}
}
<span style="color: rgb(0, 136, 0); font-weight: bold;">catch</span> (Exception ex)
{
logger?.LogError(<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">$</span><span style="background-color: rgb(255, 240, 240);">"Error copying SPO content. {ex.Message}"</span>);
}
}
</pre></td></tr></tbody></table></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-30624683968322270812020-04-03T12:01:00.004+03:002021-03-30T11:38:13.367+03:00Remove Wiki tab during provisioning of Teams channels<h3>Task</h3><p>Remove Wiki tab during provisioning of Teams channels.</p><h3>Solution</h3><p>As I’m not cloning my Team, but instead enumerating channels and tabs of the source Team used as a template, it was quite straightforward to include the Wiki tab remove logic to the stage where I’m anyway fetching Tabs for each source Team Channel.</p><p>This one includes Polly retry-logic for the DELETE operation (my first time using Polly so there’s probably lot of room for improvement). </p><p>It would also be nice to bake the Polly retry-logic into the GraphServiceClient itself instead of surrounding all calls. If you know it has already been done, please comment below. I’m always looking forward to improve my code.</p><p>I’m currently using version <a href="https://www.nuget.org/packages/Microsoft.Graph/3.21.0" target="_blank">3.21.0</a> of Microsoft.Graph library.</p><p><b>Code updated to latest version March 30, 2021.</b></p><!-- HTML generated using hilite.me --><div style="background: #ffffff; overflow:auto;width:auto;border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;"><table><tr><td><pre style="margin: 0; line-height: 125%"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89</pre></td><td><pre style="margin: 0; line-height: 125%"><span style="color: #333399; font-weight: bold">var</span> team = <span style="color: #008800; font-weight: bold">new</span> Team()
{
ODataType = <span style="color: #008800; font-weight: bold">null</span>,
MessagingSettings = sourceTeam.MessagingSettings,
FunSettings = sourceTeam.FunSettings,
GuestSettings = sourceTeam.GuestSettings,
MemberSettings = sourceTeam.MemberSettings
};
team.MessagingSettings.ODataType = <span style="color: #008800; font-weight: bold">null</span>;
team.FunSettings.ODataType = <span style="color: #008800; font-weight: bold">null</span>;
team.GuestSettings.ODataType = <span style="color: #008800; font-weight: bold">null</span>;
team.MemberSettings.ODataType = <span style="color: #008800; font-weight: bold">null</span>;
Team newTeam;
<span style="color: #008800; font-weight: bold">try</span>
{
newTeam = <span style="color: #008800; font-weight: bold">await</span> retry.ExecuteAsync(<span style="color: #008800; font-weight: bold">async</span> () =>
{
<span style="color: #008800; font-weight: bold">return</span> <span style="color: #008800; font-weight: bold">await</span> graphClient.Groups[newGroup.Id].Team
.Request()
.PutAsync(team);
});
}
<span style="color: #008800; font-weight: bold">catch</span> (Exception ex)
{
logger?.LogError(<span style="color: #FF0000; background-color: #FFAAAA">$</span><span style="background-color: #fff0f0">"Error adding team to group: {ex.Message} {newGroup.Id}"</span>);
<span style="color: #008800; font-weight: bold">return</span>;
}
<span style="color: #888888">// get new team channels</span>
<span style="color: #333399; font-weight: bold">var</span> newTeamId = newTeam.Id;
logger?.LogInformation(<span style="color: #FF0000; background-color: #FFAAAA">$</span><span style="background-color: #fff0f0">"Getting new team channels {newTeamId}"</span>);
<span style="color: #333399; font-weight: bold">var</span> newTeamChannels = <span style="color: #008800; font-weight: bold">await</span> retry.ExecuteAsync(<span style="color: #008800; font-weight: bold">async</span> () =>
{
<span style="color: #008800; font-weight: bold">return</span> <span style="color: #008800; font-weight: bold">await</span> graphClient.Teams[newTeamId].Channels.Request().GetAsync();
});
logger?.LogInformation(<span style="color: #FF0000; background-color: #FFAAAA">$</span><span style="background-color: #fff0f0">"Getting new team tabs {newTeamId}"</span>);
<span style="color: #888888">// and tabs</span>
<span style="color: #008800; font-weight: bold">foreach</span> (<span style="color: #333399; font-weight: bold">var</span> chan <span style="color: #008800; font-weight: bold">in</span> newTeamChannels)
{
chan.Tabs = <span style="color: #008800; font-weight: bold">await</span> retry.ExecuteAsync(<span style="color: #008800; font-weight: bold">async</span> () =>
{
<span style="color: #008800; font-weight: bold">return</span> <span style="color: #008800; font-weight: bold">await</span> graphClient.Teams[newTeamId].Channels[chan.Id].Tabs.Request().Expand(<span style="background-color: #fff0f0">"TeamsApp"</span>).GetAsync();
});
<span style="color: #008800; font-weight: bold">await</span> Content.RemoveWikiTab(newTeamId, chan, retry, graphClient, logger);
}
<span style="color: #888888">// Define RemoveWikiTab somewhere else</span>
<span style="color: #888888">/// <summary></span>
<span style="color: #888888">/// Removes wiki tab from given channel</span>
<span style="color: #888888">/// </summary></span>
<span style="color: #888888">/// <param name="newTeamId"></param></span>
<span style="color: #888888">/// <param name="channel"></param></span>
<span style="color: #888888">/// <param name="retry"></param></span>
<span style="color: #888888">/// <param name="graphClient"></param></span>
<span style="color: #888888">/// <param name="logger"></param></span>
<span style="color: #888888">/// <returns></returns></span>
<span style="color: #008800; font-weight: bold">public</span> <span style="color: #008800; font-weight: bold">static</span> <span style="color: #008800; font-weight: bold">async</span> Task <span style="color: #0066BB; font-weight: bold">RemoveWikiTab</span>(<span style="color: #333399; font-weight: bold">string</span> newTeamId, Microsoft.Graph.Channel channel, AsyncRetryPolicy retry, GraphServiceClient graphClient, ILogger logger)
{
<span style="color: #008800; font-weight: bold">for</span> (<span style="color: #333399; font-weight: bold">int</span> i = channel.Tabs.Count - <span style="color: #6600EE; font-weight: bold">1</span>; i >= <span style="color: #6600EE; font-weight: bold">0</span>; i--)
{
<span style="color: #008800; font-weight: bold">if</span> (channel.Tabs[i].TeamsApp.Id == TeamsAppId.Wiki)
{
<span style="color: #008800; font-weight: bold">try</span>
{
<span style="color: #008800; font-weight: bold">await</span> retry.ExecuteAsync(<span style="color: #008800; font-weight: bold">async</span> () =>
{
logger?.LogInformation(<span style="color: #FF0000; background-color: #FFAAAA">$</span><span style="background-color: #fff0f0">"Removing wiki on channel {channel.DisplayName}. {newTeamId}"</span>);
<span style="color: #008800; font-weight: bold">await</span> graphClient.Teams[newTeamId].Channels[channel.Id].Tabs[channel.Tabs[i].Id].Request().DeleteAsync();
});
channel.Tabs.RemoveAt(i);
}
<span style="color: #008800; font-weight: bold">catch</span> (Exception ex)
{
logger?.LogError(<span style="color: #FF0000; background-color: #FFAAAA">$</span><span style="background-color: #fff0f0">"Error removing wiki tab on channel: {channel.DisplayName}. {ex.Message}"</span>);
}
}
}
}
</pre></td></tr></table></div>
Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com2tag:blogger.com,1999:blog-7258322562721325894.post-36083325199683384852020-03-31T12:43:00.001+03:002020-03-31T12:48:23.057+03:00SharePoint: Story of an orphan assemblyBinding and a misleading SPSite error message<h3>Error</h3><p>I have .NET console application doing all kinds of magic, but as a first step it creates instance of SPSite using the same piece of code I’ve used hundreds, if not thousands, of times over the last two decades:</p><p><strong><font face="Courier New">using(var site = new SPSite(“http://xyz”)) </font></strong></p><p>But no, after few seconds it threw exception:</p><p><em>The Web application at http://xyz could not be found. Verify that you have typed the URL correctly. If the URL should be serving existing content, the system administrator may need to add a new request URL mapping to the intended application.<br><br></em></p><h3>Thoughts</h3><p>Having done all the suggested steps in <a href="https://blog.stefan-gossner.com/2011/09/18/common-issue-new-spsite-api-call-returns-the-web-application-at-httpserverport-could-not-be-found/" target="_blank">here</a> (including comments), and in all blog and forum posts related to this issue on the internet,<em> </em>I finally looked at ULS logs. Should’ve done it sooner. It had logged this error message when creating instance of SPSite:</p><p><em>Unexpected error while verifying backwards compatibility for [SPConfigurationDatabase] [e6f4eb31-959b-48da-a3a9-44d23f908f2e]: Microsoft.SharePoint.Upgrade.SPUpgradeException: Failed to call GetTypes on assembly Microsoft.Office.Server.Search, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c. Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies. The system cannot find the file specified. Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies. The system cannot find the file specified. Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies. The system cannot find the file specified.</em> </p><p>What on earth, I don’t use Newtonsoft.Json in my project…?!</p><p>But I did! Only for a short while, though.</p><p>During some stage of the development I had added <em>Newtonsoft.Json</em> from NuGet, then removed it. It left orphan <em>assemblyBinding</em> reference pointing to <em>Newtonsoft.Json</em> in the app.config of the console application.</p><p>So basically my <strong>app.config</strong> in Visual Studio eventually the <strong>consoleapp.exe.config</strong> looked like this:</p><p><font face="Courier New"><?xml version="1.0" encoding="utf-8"?><br>
<configuration><br> <startup> <br> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/><br> </startup><br> <runtime><br> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"><br> <dependentAssembly><br> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/><br> <bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0"/><br> </dependentAssembly><br> </assemblyBinding><br> </runtime><br>
</configuration></font></p><p>This caused the slightly misleading Exception being thrown when doing the SPSite thing.<br><br></p><h3>Solution</h3><p>I removed the orphan <assemblyBinding></assemblyBinding> under <runtime> in my app.config, rebuilt the app, and sun started shining, and all was good:</p><p><font face="Courier New"><?xml version="1.0" encoding="utf-8"?><br>
<configuration><br> <startup> <br> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/><br> </startup><br>
</configuration></font></p>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-46078378235328328932020-02-04T11:15:00.001+02:002020-02-04T11:15:38.993+02:00SharePoint: Web part SPUserCodeNoAvailableServersFoundException error<h3>Problem</h3><p>Web part is not loading, you see exception SPUserCodeNoAvailableServersFoundException.</p><h3>Solution</h3><p>Start “Microsoft SharePoint Foundation Sandboxed Code Service”.</p><ol><li>Go to Central Admin</li><li>Go to System Settings –> Manage services in this farm</li><li>At “Microsoft SharePoint Foundation Sandboxed Code Service” click “Enable Auto Provisioning”</li></ol>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-40848293577923161092020-01-30T16:10:00.001+02:002020-01-30T16:12:42.467+02:00SharePoint: Setting list view specific persisted spcolumnsize programmatically<h3>Problem</h3><p>I needed to set default list view column widths programmatically on SharePoint Online modern list. I was using PnPjs to create and configure the list.</p><h3>Thoughts</h3><p>In modern SPO list view you can change column width, it is then temporarily stored in browser Local Storage as <code>spcolumnsize-unsaved-VIEWGUID</code>. However, you also have the option of saving it into View somehow, as asterisk (*) appears next to view name on SharePoint and you can "Save View As" to persist the column width <em>somewhere</em>. After saving the view even if you remove the <code>spcolumnsize-unsaved-VIEWGUID </code>Local Storage key, and refresh the page, it comes back as <code>spcolumnsize-VIEWGUID</code>, so clearly it is stored somewhere in SharePoint.</p><h3>Solution</h3><p>Column widths are persisted in <em>ListViewXml</em> view property inside <em>ColumnWidth</em> element as <em>FieldRef </em>elements. Do note that the <em>FieldRef</em> Names must be column display names, NOT internal field names. I didn’t implement fancy XML parsing, just injected the <em>ColumnWidth</em> element at the end just before ending <em>View</em> tag.<p><font face="Courier New">// set field widths<br>let lv: any = await list.defaultView.select('ListViewXml').get();</font><p><font face="Courier New">let lvXml: string = lv.ListViewXml.replace(<br>'</View>', '<ColumnWidth><FieldRef Name="Column title 1" width="370" /><FieldRef Name="Some other column" width="90" /></ColumnWidth></View>');<br><br>await list.defaultView.setViewXml(lvXml);</font></p>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-71651143616598154222019-12-12T12:15:00.001+02:002019-12-12T12:41:22.706+02:00SPFx: Changing Folder Content Type using PnPjs<h3>Task</h3><p>Needed to create Folders with custom Content Type in SharePoint document library from my SPFx web part, as there was a need to add some custom fields, such as Description, to the Folder item.</p><h3>Solution</h3><p>This one took a few hours to figure out, you need to do it in few steps as you cannot change the content type of a folder using <font face="Courier New">sp.web.folders…update</font> as REST API doesn’t allow <em>ContentTypeId</em> parameter when updating Folder content types, and you will get error:</p><blockquote><p><font color="#ff0000">“The property 'ContentTypeId' does not exist on type 'SP.Folder’. Make sure to only use property names that are defined by the type”</font></p></blockquote><p>Also if you would use <font face="Courier New">…folder.getItem()</font><font face="Arial">, it will fail if folder has special characters.</font></p><ol><li>Create folder as normal Folder</li><li>Get list item ID of the folder</li><li>Update folder as a list item</li></ol><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(0, 136, 0); font-weight: bold;">let</span> etfn <span style="color: rgb(51, 51, 51);">=</span> await sp.web.getList(listUrl).getListItemEntityTypeFullName();
<span style="color: rgb(136, 136, 136);">// first create folder</span>
<span style="color: rgb(0, 136, 0); font-weight: bold;">let</span> far: <span style="color: rgb(51, 51, 153); font-weight: bold;">FolderAddResult</span> <span style="color: rgb(51, 51, 51);">=</span> await sp.web.folders.add(listUrl <span style="color: rgb(51, 51, 51);">+</span> <span style="background-color: rgb(255, 240, 240);">'/'</span> <span style="color: rgb(51, 51, 51);">+</span> targetParentFolderName <span style="color: rgb(51, 51, 51);">+</span> targetFolderName);
<span style="color: rgb(136, 136, 136);">// then get list item ID of the folder</span>
<span style="color: rgb(0, 136, 0); font-weight: bold;">let</span> fData: <span style="color: rgb(51, 51, 153); font-weight: bold;">any</span> <span style="color: rgb(51, 51, 51);">=</span> await sp.web.getFolderById(far.data.UniqueId).select(<span style="background-color: rgb(255, 240, 240);">'ID'</span>).listItemAllFields.get();
<span style="color: rgb(136, 136, 136);">// then get folder as list item</span>
<span style="color: rgb(0, 136, 0); font-weight: bold;">let</span> item: <span style="color: rgb(51, 51, 153); font-weight: bold;">Item</span> <span style="color: rgb(51, 51, 51);">=</span> sp.web.getList(listUrl).items.getById(fData[<span style="background-color: rgb(255, 240, 240);">'ID'</span>]);
await item.update({
ContentTypeId<span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'CUSTOM_FOLDER_CONTENTTYPE'</span>,
PF_FolderDescription<span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'Some description...'</span>
}, <span style="background-color: rgb(255, 240, 240);">'*'</span>, etfn);
</pre></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-1274291445295557092019-09-17T11:53:00.001+03:002019-09-17T11:58:44.945+03:00Office Online Server: X-WOPI-ServerError "Verifying signature failed"<h3>
Problem</h3>
After migrating from old OOS (WAC) server to new one, integration between SharePoint and Office Online Server stopped working, and no documents could be opened in browser.
<br />
<br />
Looking at the network traffic using WireShark, you see X-WOPI-ServerError "Verifying signature failed".<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkM4nMDFkChsff7GWav9TQgyQLVvN6lkRBca1CYBi4j1vNraEl0Ful8cig3gEqGSvD9Kj9_SSMdXP-D5HtWrz8xxqhvvS7fGx8E-JKKWIPVmTlyKhQUGIWd3wWAOBylB7dXOC9ILSmwQsn/s1600/oos.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="368" data-original-width="800" height="147" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkM4nMDFkChsff7GWav9TQgyQLVvN6lkRBca1CYBi4j1vNraEl0Ful8cig3gEqGSvD9Kj9_SSMdXP-D5HtWrz8xxqhvvS7fGx8E-JKKWIPVmTlyKhQUGIWd3wWAOBylB7dXOC9ILSmwQsn/s320/oos.png" width="320" /></a></div>
<br />
<h3>
Solution</h3>
<ol>
<li>Ensure HTTPS (TCP 443) flows from SP servers to OOS</li>
<li>Run <span style="font-family: "courier new";">Update-SPWOPIProofKey -ServerName YOUR_OOS_URL</span></li>
</ol>
This will update the required keys in SharePoint and documents can again be opened in browser.Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-55258821039017740702019-09-04T16:16:00.000+03:002019-09-04T16:18:27.288+03:00SharePoint Online: Hiding "Shared with Us" link in Modern Menu<h3>
Problem</h3>
<a href="https://www.blogger.com/blogger.g?blogID=7258322562721325894"></a>For <a href="https://techcommunity.microsoft.com/t5/SharePoint/quot-Shared-with-Us-quot-link-in-Modern-SharePoint-Menu-behavior/m-p/41692">apparently nearly 3 years</a>, people have wanted to hide the “Shared with Us” menu item that is displayed in the Current navigation on modern SharePoint list views. The menu item is injected as 4th item on the menu on sites that are Group based. Item doesn’t exist on traditional Team sites although they would be modern.<span style="font-family: "arial" , "helvetica" , sans-serif;"><br /></span>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjjtW-_VCA69YCAo7ttxSePS2YhYoPZJrGdJ81WFvktUTc5g2ipDX8twky2ZkurQyM_-nDRPaA1A173KaMoitM_RBIGV8YyHb6dt7QyrQA5VRcNfgpeyVZRP6ZY10BNUG7d7HTADrWmfWbv/s1600/swu2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><span style="font-family: "arial" , "helvetica" , sans-serif;"><img border="0" data-original-height="158" data-original-width="175" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjjtW-_VCA69YCAo7ttxSePS2YhYoPZJrGdJ81WFvktUTc5g2ipDX8twky2ZkurQyM_-nDRPaA1A173KaMoitM_RBIGV8YyHb6dt7QyrQA5VRcNfgpeyVZRP6ZY10BNUG7d7HTADrWmfWbv/s1600/swu2.png" /></span></a></div>
<br />
<br />
<h3>
Thoughts</h3>
<a href="https://www.blogger.com/blogger.g?blogID=7258322562721325894"></a>There is no way to hide it by modifying the Current navigation, nor by disabling any feature.<br />
<div>
<a href="https://www.blogger.com/blogger.g?blogID=7258322562721325894" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"></a><a href="https://www.blogger.com/$image[4].png" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"></a><br />
<h3>
Solution</h3>
Best I came up with was to inject CSS on all tenant sites, and hide it with simple CSS style. It’s not perfect solution, as the CSS is injected using JavaScript meaning when page is loaded, it will momentarily display the “Shared with us” link before the JavaScript is loaded and CSS is injected. <br />
<br />
Good thing, though, is that it does work across all sites with just one deploy and can be easily modified.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFSmuNheLrHJIMUD06KBsCPhVPIJdIZbau82CIW5g-O92FGYz3ex_w3v13ifSSkNkyarhhFh2nJss0lVZaDmjunXfR3DoCT8XQMPt-0temSpeW7zXbSjgg_K9WUCdBj-lE_V2DkbHLEdS0/s1600/swu.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><span style="font-family: "arial" , "helvetica" , sans-serif;"><img border="0" data-original-height="75" data-original-width="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFSmuNheLrHJIMUD06KBsCPhVPIJdIZbau82CIW5g-O92FGYz3ex_w3v13ifSSkNkyarhhFh2nJss0lVZaDmjunXfR3DoCT8XQMPt-0temSpeW7zXbSjgg_K9WUCdBj-lE_V2DkbHLEdS0/s1600/swu.png" /></span></a></div>
<h3>
<strong><span style="font-family: "arial" , "helvetica" , sans-serif;">Let’s do this</span></strong></h3>
<ol>
<li><span style="font-family: "arial" , "helvetica" , sans-serif;">Clone and build <a href="https://github.com/jpalo/react-application-injectcss" target="_blank">my fork</a> (<a href="https://github.com/jpalo/react-application-injectcss">https://github.com/jpalo/react-application-injectcss</a>) of the handy CSS injection SPFx extension made originally by <a href="https://github.com/hugoabernier/react-application-injectcss" target="_blank">hugoabernier</a>, thanks Hugo! <br />- Hugo’s latest version is for SPFx 1.8.0, mine is 1.9.1, so in case you want to use the latest (as of writing) SPFx version, use my fork. <br />- My code also assumes the custom.css is in <em>/sites/cdn/Style%20Library/custom.css</em>, but that is easily modified in case you want to use another location.</span></li>
<li><span style="font-family: "arial" , "helvetica" , sans-serif;">Before deploying the .sspkg to the tenant, ensure<strong> /sites/cds </strong>site collection exists, and upload the <em>custom.css</em> from the of the SPFx project to <em>/sites/cdn/Style%20Library/custom.css</em></span></li>
<li><span style="font-family: "arial" , "helvetica" , sans-serif;">Depending on the user base of your tenant, you will need to assign proper <strong>Read</strong> permissions to the <strong>/sites/cdn</strong> site collection for all users (and possibly Guests) to be able to read the <em>custom.css</em> file</span></li>
</ol>
<br /></div>
Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com2tag:blogger.com,1999:blog-7258322562721325894.post-64524097249225173062019-05-22T18:29:00.001+03:002019-05-22T18:29:00.767+03:00SharePoint: Determining if library root level folders have broken permission inheritance using PnP PowerShell<p>This simple script can be used to determine status of permission inheritance of document library root level folders.</p><p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(153, 102, 51);">$siteUrl</span> = <span style="background-color: rgb(255, 240, 240);">"https://mytenant.sharepoint.com/sites/somesitecollection"</span>
<span style="color: rgb(0, 112, 32);">Connect-PnPOnline</span> -Url <span style="color: rgb(153, 102, 51);">$siteUrl</span> -UseWebLogin
<span style="color: rgb(153, 102, 51);">$context</span> = <span style="color: rgb(0, 112, 32);">Get-PnPContext</span>
<span style="color: rgb(153, 102, 51);">$list</span> = <span style="color: rgb(0, 112, 32);">Get-PnPList</span> <span style="background-color: rgb(255, 240, 240);">"SomeLibrary"</span>
<span style="color: rgb(153, 102, 51);">$folders</span> = <span style="color: rgb(153, 102, 51);">$list</span>.RootFolder.Folders
<span style="color: rgb(153, 102, 51);">$context</span>.Load(<span style="color: rgb(153, 102, 51);">$folders</span>)
<span style="color: rgb(153, 102, 51);">$context</span>.ExecuteQuery()
<span style="color: rgb(0, 136, 0); font-weight: bold;">foreach</span>(<span style="color: rgb(153, 102, 51);">$folder</span> <span style="color: rgb(0, 136, 0); font-weight: bold;">in</span> <span style="color: rgb(153, 102, 51);">$folders</span>)
{
<span style="color: rgb(0, 136, 0); font-weight: bold;">if</span>(<span style="color: rgb(153, 102, 51);">$folder</span>.ItemCount <span style="color: rgb(51, 51, 51);">-gt</span> 0)
{
<span style="color: rgb(153, 102, 51);">$f</span> = <span style="color: rgb(0, 112, 32);">Get-PnPFolder</span> -Url <span style="color: rgb(153, 102, 51);">$folder</span>.ServerRelativeUrl -Includes ListItemAllFields.RoleAssignments, ListItemAllFields.HasUniqueRoleAssignments
<span style="color: rgb(153, 102, 51);">$context</span>.Load(<span style="color: rgb(153, 102, 51);">$f</span>)
<span style="color: rgb(153, 102, 51);">$context</span>.ExecuteQuery()
<span style="color: rgb(0, 112, 32);">Write-Host</span> <span style="color: rgb(153, 102, 51);">$f</span>.ServerRelativeUrl -> <span style="color: rgb(153, 102, 51);">$f</span>.ListItemAllFields.HasUniqueRoleAssignments
}
}
</pre></td></tr></tbody></table></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0tag:blogger.com,1999:blog-7258322562721325894.post-34877657221576234482019-05-20T15:39:00.001+03:002019-05-20T15:55:15.562+03:00SharePoint: Update List Content Type via REST from SPFx web part<h3>Task</h3><p>I needed to set <em>ReadOnly</em> property of a SPList Content Type from my SPFx web part. I’m using <a href="https://github.com/pnp/pnpjs/blob/dev/packages/sp/docs/index.md" target="_blank">@pnp/sp</a> library, but it doesn’t support modifying existing Content Types.</p><h3>Solution</h3><p>Changing the <em>ReadOnly</em> property of an existing list content type is possible using REST, but I had some troubles finding out correct set of HTTP body and header payloads. Working code can be found below.</p><p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;"> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(0, 136, 0); font-weight: bold;">const</span> spOpts: <span style="color: rgb(51, 51, 153); font-weight: bold;">ISPHttpClientOptions</span> <span style="color: rgb(51, 51, 51);">=</span> {
headers<span style="color: rgb(51, 51, 51);">:</span> { <span style="background-color: rgb(255, 240, 240);">'Accept'</span><span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'application/json;odata=verbose'</span>, <span style="background-color: rgb(255, 240, 240);">'X-HTTP-Method'</span><span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'MERGE'</span>, <span style="background-color: rgb(255, 240, 240);">'odata-version'</span><span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'3.0'</span> },
body: <span style="color: rgb(51, 51, 153); font-weight: bold;">JSON.stringify</span>({
__metadata<span style="color: rgb(51, 51, 51);">:</span> {
type<span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'SP.ContentType'</span>
},
ReadOnly: <span style="color: rgb(51, 51, 153); font-weight: bold;">false</span>
})
};
<span style="color: rgb(0, 136, 0); font-weight: bold;">let</span> oldCt: <span style="color: rgb(51, 51, 153); font-weight: bold;">ContentType</span> <span style="color: rgb(51, 51, 51);">=</span> list.contentTypes.getById(<span style="background-color: rgb(255, 240, 240);">'0x01CONTENTTYPEID'</span>);
await <span style="color: rgb(0, 136, 0); font-weight: bold;">this</span>.props.context.spHttpClient.post(
oldCt.toUrlAndQuery(),
SPHttpClient.configurations.v1,
spOpts
);
</pre></td></tr></tbody></table></div><table border="0" cellspacing="0" cellpadding="2">
<tbody>
<tr>
<td valign="top"><h3><br></h3><h3>Thoughts</h3><p>For later reference when investigating similar issues, here’s a collection of error messages depending what Header parameter is missing or invalid.</p><table border="1" cellspacing="0" cellpadding="2">
<tbody>
<tr><td valign="top"><strong>Without this header property</strong></td><td valign="top"><strong>You got error</strong></td></tr><tr>
<td valign="top"><span style="background-color: rgb(255, 240, 240);">'Accept'</span><span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'application/json;odata=verbose'</span></td>
<td valign="top"><em>The property '__metadata' does not exist on type 'SP.ContentType'. Make sure to only use property names that are defined by the type.</em><br><br><strong>Note: </strong>This is not required, as odata will be <em>verbose</em> by default <strong>unless</strong> you have manually set it to, e.g., <em>nometadata</em>. I usually set it to <em>nometadata</em> to improve performance as verbose metadata results of REST calls are not usually required. <em>odata=minimalmetadata</em> is not enough.</td>
</tr>
<tr>
<td valign="top"><span style="background-color: rgb(255, 240, 240);">'X-HTTP-Method'</span><span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'MERGE'</span></td>
<td valign="top"><em>The parameter __metadata does not exist in method GetById.</em></td>
</tr>
<tr>
<td valign="top"><span style="background-color: rgb(255, 240, 240);">'odata-version': ‘3.0’</span></td>
<td valign="top"><em>Parsing JSON Light feeds or entries in requests without entity set is not supported. Pass in the entity set as a parameter to ODataMessageReader.CreateODataEntryReader or ODataMessageReader.CreateODataFeedReader method.<br><br></em><strong>Note: </strong>By default, odata-version will be 4.0, and it doesn’t work here.</td>
</tr>
</tbody>
</table>
</td>
<td valign="top"></td>
</tr>
<tr>
<td valign="top"></td>
<td valign="top"></td>
</tr>
<tr>
<td valign="top"></td>
<td valign="top"></td>
</tr>
</tbody>
</table>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com1tag:blogger.com,1999:blog-7258322562721325894.post-48050933189397182652019-05-02T13:02:00.001+03:002019-05-02T13:02:49.989+03:00SharePoint: spHttpClient search query returns HTTP 500 when requesting Created field<h2>Problem</h2><p>When making SharePoint search query programmatically via REST API and including <em>Created</em> in <em>selectProperties</em>, query returns HTTP 500.</p><p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;">1
2
3
4
</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(0, 136, 0); font-weight: bold;">this</span>.webPartContext.spHttpClient.get(
<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">`</span>${<span style="color: rgb(0, 136, 0); font-weight: bold;">this</span>._searchUrl}<span style="color: rgb(51, 51, 51);">?</span>querytext<span style="color: rgb(51, 51, 51);">=</span><span style="background-color: rgb(255, 240, 240);">'${query}'</span><span style="color: rgb(51, 51, 51);">&</span>selectProperties<span style="color: rgb(51, 51, 51);">=</span><span style="background-color: rgb(255, 240, 240);">'Title,Path,Created'</span><span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">`</span>,
SPHttpClient.configurations.v1
)
</pre></td></tr></tbody></table></div>
<p>When making the query via browser, it works nicely with and without Created in selectProperties.</p><h2>Solution</h2><p>Include specific <em>ISPHttpOptions</em> in the get call makes it work as described below.</p><p>In any case, it is good idea to include the <em>odata=nometadata</em> option in your REST calls to enable <a href="https://www.microsoft.com/en-us/microsoft-365/blog/2014/08/13/json-light-support-rest-sharepoint-api-released/">JSON Light</a> whenever applicable to minimize the return payload. Usually you’re not interested in the metadata anyway.</p><p><!-- HTML generated using hilite.me --><div style="background: rgb(255, 255, 255); border-width: 0.1em 0.1em 0.1em 0.8em; border-style: solid; border-color: gray; padding: 0.2em 0.6em; border-image: none; width: auto; overflow: auto;"><table><tbody><tr><td><pre style="margin: 0px; line-height: 125%;"> 1
2
3
4
5
6
7
8
9
10
11</pre></td><td><pre style="margin: 0px; line-height: 125%;"><span style="color: rgb(136, 136, 136);">// define _noMetaOpt</span>
<span style="color: rgb(0, 136, 0); font-weight: bold;">private</span> _nometaOpt: <span style="color: rgb(51, 51, 153); font-weight: bold;">ISPHttpClientOptions</span> <span style="color: rgb(51, 51, 51);">=</span> {
headers<span style="color: rgb(51, 51, 51);">:</span> { <span style="background-color: rgb(255, 240, 240);">'Accept'</span><span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">'application/json;odata=nometadata'</span>, <span style="background-color: rgb(255, 240, 240);">'odata-version'</span><span style="color: rgb(51, 51, 51);">:</span> <span style="background-color: rgb(255, 240, 240);">''</span> }
};
<span style="color: rgb(136, 136, 136);">// and call</span>
<span style="color: rgb(0, 136, 0); font-weight: bold;">this</span>.webPartContext.spHttpClient.get(
<span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">`</span>${<span style="color: rgb(0, 136, 0); font-weight: bold;">this</span>._searchUrl}<span style="color: rgb(51, 51, 51);">?</span>querytext<span style="color: rgb(51, 51, 51);">=</span><span style="background-color: rgb(255, 240, 240);">'${query}'</span><span style="color: rgb(51, 51, 51);">&</span>selectProperties<span style="color: rgb(51, 51, 51);">=</span><span style="background-color: rgb(255, 240, 240);">'Title,Path,Created'</span><span style="color: rgb(255, 0, 0); background-color: rgb(255, 170, 170);">`</span>,
SPHttpClient.configurations.v1,
<span style="color: rgb(0, 136, 0); font-weight: bold;">this</span>._nometaOpt
)
</pre></td></tr></tbody></table></div>Jussi Palohttp://www.blogger.com/profile/00392327346825133905noreply@blogger.com0