<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[SharePoint SPFx Development by $€®¥09@]]></title><description><![CDATA[SharePoint SPFx Development by $€®¥09@]]></description><link>https://spfx-app.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1629447780219/lWWb3w99Y.png</url><title>SharePoint SPFx Development by $€®¥09@</title><link>https://spfx-app.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 04:57:37 GMT</lastBuildDate><atom:link href="https://spfx-app.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[@spfxappdev/cli Version 2.0.0 is Here]]></title><description><![CDATA[I am very excited to announce that version 2.0.0 of the @spfxappdev/cli is officially released.
While there are a few smaller updates under the hood, this major release focuses on one very important b]]></description><link>https://spfx-app.dev/spfxappdev-cli-version-2-0-0-is-here</link><guid isPermaLink="true">https://spfx-app.dev/spfxappdev-cli-version-2-0-0-is-here</guid><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Wed, 25 Feb 2026 14:03:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/610b7bdbee887d4cb4965ca5/60f97a4e-d654-4c62-87dd-b97e311de692.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am very excited to announce that version 2.0.0 of the <strong>@spfxappdev/cli</strong> is officially released.</p>
<p>While there are a few smaller updates under the hood, this major release focuses on one very important breaking change: <strong>The way the CLI authenticates with SharePoint and Microsoft 365.</strong></p>
<p>If you use the CLI to automatically generate models based on your SharePoint lists, this article is for you. Here is everything you need to know about the new authentication process.</p>
<h2>Goodbye <code>sp-request</code> (Username &amp; Password)</h2>
<p>In previous versions, the CLI used the <code>sp-request</code> package. This allowed you to connect to your SharePoint environment by simply typing in your username and password.</p>
<p>While this was very easy to use, it had some big problems:</p>
<ol>
<li><p><strong>Security:</strong> Sending a username and password directly is no longer recommended.</p>
</li>
<li><p><strong>MFA (Multi-Factor Authentication):</strong> Username and password logins often fail if your organization uses MFA or conditional access policies. Microsoft is actively disabling basic authentication across all tenants.</p>
</li>
</ol>
<p>To make the CLI future-proof and much more secure, username and password authentication is <strong>no longer supported</strong> in version 2.0.0.</p>
<h2>Welcome <code>msal-node</code> (Modern Authentication)</h2>
<p>To replace the old method, the CLI now uses the official Microsoft Authentication Library for Node.js (<code>@azure/msal-node</code>).</p>
<p>Instead of a user account, you now need an <strong>App Registration</strong> in Microsoft Entra ID (formerly Azure AD). This is the modern, secure way to connect to Microsoft 365.</p>
<p>Depending on your needs, you can now choose between two different authentication methods:</p>
<h3>1. The Device Code Flow (Easy &amp; Interactive)</h3>
<p>If you only provide a <code>Client ID</code> to the CLI, it will use the <strong>Device Code Flow</strong>.</p>
<p><strong>How it works:</strong> The CLI will show you a short code and a Microsoft login URL in your terminal. You just open the link in your browser, enter the code, and log in securely with your normal Microsoft 365 account (MFA works perfectly here!).</p>
<ul>
<li><strong>Behind the scenes:</strong> This method uses <strong>Delegated Permissions</strong> and connects to the <strong>SharePoint REST API</strong>. The authentication is cached, so you don't have to log in every single time.</li>
</ul>
<h3>2. The Client Credentials Flow (Automated &amp; Background)</h3>
<p>If you provide both a <code>Client ID</code> and a <code>Client Secret</code> to the CLI, it will use the <strong>Client Credentials Flow</strong>.</p>
<p><strong>How it works:</strong> The CLI logs in automatically in the background without any browser interaction. This is great if you don't want to be interrupted or if you are running scripts automatically.</p>
<ul>
<li><strong>Behind the scenes:</strong> This method uses <strong>Application Permissions</strong> (App-Only) and connects to the <strong>Microsoft Graph API</strong>.</li>
</ul>
<h3>Important: SharePoint REST API vs. Microsoft Graph API</h3>
<p>Because the two flows use different APIs under the hood, there is a small difference in the models that the CLI generates for you:</p>
<ul>
<li><p><strong>When using Device Code (SharePoint REST API):</strong> The CLI gets full details about your SharePoint lists. It can read hidden fields and knows the exact types of complex fields (like Taxonomy or URL fields).</p>
</li>
<li><p><strong>When using Client Credentials (Graph API):</strong> The Graph API is very fast, but it does not return all field details. Hidden fields cannot be read, and complex fields (like Taxonomy) might be typed as <code>any</code> in your generated TypeScript model.</p>
</li>
</ul>
<p>If you need strict type safety for complex fields, I highly recommend using the <strong>Device Code Flow</strong> (leaving the <code>Client Secret</code> empty).</p>
<h2>Ready to upgrade?</h2>
<p>You can install the new version globally via npm right now:</p>
<p>Bash</p>
<pre><code class="language-shell">npm i @spfxappdev/cli -g
</code></pre>
<p>To learn exactly how to set up your App Registration in Azure and configure the CLI, please check out the updated documentation on the <a href="https://github.com/SPFxAppDev/spfxcli">GitHub repository</a></p>
<p>Thank you for using the CLI, and happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[How to get the SharePoint Footer Logo in SPFx]]></title><description><![CDATA[I'm currently developing a custom SPFx Application Customizer to replace the standard SharePoint footer. A key requirement for my custom footer was to include a logo, but I didn't want to use a static, hardcoded image. Instead, my goal was to dynamic...]]></description><link>https://spfx-app.dev/how-to-get-the-sharepoint-footer-logo-in-spfx</link><guid isPermaLink="true">https://spfx-app.dev/how-to-get-the-sharepoint-footer-logo-in-spfx</guid><category><![CDATA[Microsoft]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[SharePoint Online]]></category><category><![CDATA[SharePointFramework]]></category><category><![CDATA[PnP]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Tue, 07 Oct 2025 08:07:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759824200107/c381be36-d90f-48da-8aac-13a4667c55b0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm currently developing a custom SPFx Application Customizer to replace the standard SharePoint footer. A key requirement for my custom footer was to include a logo, but I didn't want to use a static, hardcoded image. Instead, my goal was to dynamically use the same logo configured in the site's official footer settings.</p>
<h2 id="heading-the-investigation">The Investigation</h2>
<p>I started by searching for an official API endpoint to get the footer logo URL. After scouring the internet and the official API documentation, I found that there was absolutely no information available - no articles, no forum posts, nothing.</p>
<p>So, I decided to take a closer look at the standard footer itself and analyze the API calls it makes. To my surprise, I found that it uses the well-known <code>getMenuState</code> function from the navigation API (PnPJS), but with a specific menu node key. In this case, the key is a static GUID: <code>13b7c916-4fea-4bb2-8994-5cf274aeb530</code>.</p>
<h2 id="heading-the-breakthrough-all-data-is-stored-in-the-navigation">The breakthrough: All data is stored in the navigation</h2>
<p>It turns out SharePoint stores the entire footer configuration the logo, the footer name, and the navigation nodes as a “hidden” navigation menu. By querying this menu, I discovered that special items like the logo and name are identified by nodes with static GUIDs as their titles.</p>
<ul>
<li><p><strong>Footer Logo</strong>: The node for the logo always has the title <code>2e456c2e-3ded-4a6c-a9ea-f7ac4c1b5100</code>. The URL to the image is stored in its <code>SimpleUrl</code> property.</p>
</li>
<li><p><strong>Footer Name</strong>: The node for the footer's name has the title <code>7376cd83-67ac-4753-b156-6a7b3fa0fc1f</code>.</p>
</li>
<li><p><strong>Footer Links/Nodes</strong>: The "real" navigation links are stored as children under the node with the title <code>3a94b35f-030b-468e-80e3-b75ee84ae0ad</code>.</p>
</li>
</ul>
<h2 id="heading-response-json-example">Response JSON Example</h2>
<p>To make it clearer, here is a shortened example of the JSON response from the <code>getMenuState</code> API call:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"@odata.context"</span>: <span class="hljs-string">"https://sscwebdev.sharepoint.com/sites/arano/_api/$metadata#SP.MenuState"</span>,
  <span class="hljs-comment">//...</span>
  <span class="hljs-attr">"Nodes"</span>: [
    {
      <span class="hljs-comment">//...</span>
      <span class="hljs-attr">"Nodes"</span>: [
        {
          <span class="hljs-comment">//...</span>
          <span class="hljs-attr">"OpenInNewWindow"</span>: <span class="hljs-literal">null</span>,
          <span class="hljs-attr">"SimpleUrl"</span>: <span class="hljs-string">""</span>,
          <span class="hljs-attr">"Title"</span>: <span class="hljs-string">"Footer Name"</span>,
          <span class="hljs-attr">"Translations"</span>: []
        }
      ],
      <span class="hljs-attr">"Title"</span>: <span class="hljs-string">"7376cd83-67ac-4753-b156-6a7b3fa0fc1f"</span>, <span class="hljs-comment">//ID for Footer Name</span>
    },
    {
      <span class="hljs-comment">//...</span>
      <span class="hljs-attr">"OpenInNewWindow"</span>: <span class="hljs-literal">null</span>,
      <span class="hljs-attr">"SimpleUrl"</span>: <span class="hljs-string">"/sites/site1/SiteAssets/__footerlogo__site1.png"</span>,
      <span class="hljs-attr">"Title"</span>: <span class="hljs-string">"2e456c2e-3ded-4a6c-a9ea-f7ac4c1b5100"</span>, <span class="hljs-comment">//ID for Footer Logo</span>
      <span class="hljs-attr">"Translations"</span>: []
    },
    {
      <span class="hljs-comment">//...</span>
      <span class="hljs-attr">"Nodes"</span>: [
        {
          <span class="hljs-attr">"AudienceIds"</span>: [],
          <span class="hljs-attr">"CurrentLCID"</span>: <span class="hljs-number">1033</span>,
          <span class="hljs-attr">"CustomProperties"</span>: [],
          <span class="hljs-attr">"FriendlyUrlSegment"</span>: <span class="hljs-string">""</span>,
          <span class="hljs-attr">"IsDeleted"</span>: <span class="hljs-literal">false</span>,
          <span class="hljs-attr">"IsHidden"</span>: <span class="hljs-literal">false</span>,
          <span class="hljs-attr">"IsTitleForExistingLanguage"</span>: <span class="hljs-literal">false</span>,
          <span class="hljs-attr">"Key"</span>: <span class="hljs-string">"2018"</span>,
          <span class="hljs-attr">"Nodes"</span>: [],
          <span class="hljs-attr">"NodeType"</span>: <span class="hljs-number">0</span>,
          <span class="hljs-attr">"OpenInNewWindow"</span>: <span class="hljs-literal">false</span>,
          <span class="hljs-attr">"SimpleUrl"</span>: <span class="hljs-string">"http://google.de"</span>,
          <span class="hljs-attr">"Title"</span>: <span class="hljs-string">"My Footer Nav Item 1"</span>,
          <span class="hljs-attr">"Translations"</span>: []
        }
      ],
      <span class="hljs-attr">"NodeType"</span>: <span class="hljs-number">0</span>,
      <span class="hljs-attr">"OpenInNewWindow"</span>: <span class="hljs-literal">null</span>,
      <span class="hljs-attr">"SimpleUrl"</span>: <span class="hljs-string">"/sites/site1"</span>,
      <span class="hljs-attr">"Title"</span>: <span class="hljs-string">"3a94b35f-030b-468e-80e3-b75ee84ae0ad"</span>, <span class="hljs-comment">//ID for all other Nodes for Footer as "Subnodes"</span>
      <span class="hljs-attr">"Translations"</span>: []
    }
  ],
  <span class="hljs-attr">"SimpleUrl"</span>: <span class="hljs-string">"/sites/site1"</span>,
}
</code></pre>
<h2 id="heading-pnpjs-implementation">PnPJS Implementation</h2>
<p>With this knowledge, it's easy to create a function using PnPJS to fetch and parse this data. Here is a simplified example without full error handling:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//All other imports...</span>
<span class="hljs-keyword">import</span> { ISPFXContext, spfi, SPFI, SPFx } <span class="hljs-keyword">from</span> <span class="hljs-string">'@pnp/sp'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'@pnp/sp/webs'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'@pnp/sp/navigation'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> FooterApplicationCustomizer <span class="hljs-keyword">extends</span> BaseApplicationCustomizer&lt;IFooterApplicationCustomizerProperties&gt; {
    <span class="hljs-comment">//All other Code</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> onInit(): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.onInit();
        <span class="hljs-keyword">const</span> sp: SPFI = spfi().using(SPFx(<span class="hljs-built_in">this</span>.context <span class="hljs-keyword">as</span> ISPFXContext));
        <span class="hljs-keyword">const</span> footerNodes = <span class="hljs-keyword">await</span> sp.navigation.getMenuState(<span class="hljs-string">'13b7c916-4fea-4bb2-8994-5cf274aeb530'</span>);
        <span class="hljs-keyword">const</span> footerLogoNode = footerNodes.Nodes[<span class="hljs-number">0</span>].find(<span class="hljs-function"><span class="hljs-params">node</span> =&gt;</span> node.Title === <span class="hljs-string">'2e456c2e-3ded-4a6c-a9ea-f7ac4c1b5100'</span>);
        <span class="hljs-keyword">const</span> footerLogoUrl = footerLogoNode ? footerLogoNode.SimpleUrl : <span class="hljs-literal">null</span>;
    }
}
</code></pre>
<p>Of course, you don't need PnPJS for this. The whole thing also works with a direct REST API call.</p>
<p>To do this, simply call the following endpoint, which you can also paste directly into your browser to test it:</p>
<p><code>[your-sharepoint-site]/_api/navigation/MenuState?menuNodeKey='13b7c916-4fea-4bb2-8994-5cf274aeb530'</code></p>
<p>Happy Coding ;)</p>
]]></content:encoded></item><item><title><![CDATA[Simple Drag-and-Drop Sorting for React]]></title><description><![CDATA[Have you ever needed a simple drag-and-drop sorting feature in your React app, but got overwhelmed by large libraries with unnecessary features? If you're like me, especially when working on SPFx projects, you just want users to rearrange list items—...]]></description><link>https://spfx-app.dev/simple-drag-and-drop-sorting-for-react</link><guid isPermaLink="true">https://spfx-app.dev/simple-drag-and-drop-sorting-for-react</guid><category><![CDATA[React]]></category><category><![CDATA[sPfx]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[SharePointFramework]]></category><category><![CDATA[SharePointOnline]]></category><category><![CDATA[SharePoint Online]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[npm]]></category><category><![CDATA[sorting]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Wed, 26 Feb 2025 09:50:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740563331258/6cd8bf7a-4d32-421d-bf56-85e59d70a0c7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever needed a simple drag-and-drop sorting feature in your React app, but got overwhelmed by large libraries with unnecessary features? If you're like me, especially when working on SPFx projects, you just want users to rearrange list items—nothing complicated. That's why I created <a target="_blank" href="https://www.npmjs.com/package/@spfxappdev/sortable">@spfxappdev/sortable</a>, a lightweight and easy solution for basic sorting needs.</p>
<h2 id="heading-the-problem-overkill-libraries"><strong>The Problem: Overkill Libraries</strong></h2>
<p>There are a variety of drag-and-drop libraries for React. But many of them have too much extra stuff: complex animations, lots of settings and large file sizes. For simple tasks, like reordering a list of elements in an SPFx component, these libraries can be too much.</p>
<h2 id="heading-the-solution-spfxappdevsortable-keep-it-simple"><strong>The Solution:</strong> <code>@spfxappdev/sortable</code> - Keep It Simple</h2>
<p><code>@spfxappdev/sortable</code> is designed to be the opposite of these feature-rich libraries. It's a minimalist React component that provides basic drag-and-drop sorting functionality without any extras. Here's what makes it stand out:</p>
<ul>
<li><p><strong>Lightweight:</strong> Small bundle size, ensuring your application stays lean (unpacked <strong>~62kb</strong>).</p>
</li>
<li><p><strong>Simple API:</strong> Easy to use and integrate into your existing projects.</p>
</li>
<li><p><strong>No Dependencies:</strong> Zero external dependencies, minimizing potential conflicts (except React, of course).</p>
</li>
<li><p><strong>Basic Functionality:</strong> Focuses on the core task of reordering elements.</p>
</li>
</ul>
<h2 id="heading-why-i-built-it-spfx-and-beyond"><strong>Why I Built It: SPFx and Beyond</strong></h2>
<p>My main reason for creating <code>@spfxappdev/sortable</code> was my frequent need for simple sorting in SharePoint Framework (SPFx) projects. I often found myself wanting to give users the ability to customize the order of items in a list, but I didn't want to add a large and complex library to my project.</p>
<p>However, <code>@spfxappdev/sortable</code> isn't just for SPFx. It's a flexible solution for any React project where you need basic drag-and-drop sorting without the extra weight.</p>
<h2 id="heading-how-to-use-spfxappdevsortable"><strong>How to Use</strong> <code>@spfxappdev/sortable</code></h2>
<p>Using <code>@spfxappdev/sortable</code> is very easy. Here's a quick example:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {
  Sortable
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@spfxappdev/sortable"</span>;

<span class="hljs-keyword">interface</span> Item {
  id: <span class="hljs-built_in">number</span>;
  text: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">const</span> SimpleList: React.FunctionComponent = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [items, setItems] = React.useState&lt;Item[]&gt;([
    { id: <span class="hljs-number">1</span>, text: <span class="hljs-string">"Item 1"</span> },
    { id: <span class="hljs-number">2</span>, text: <span class="hljs-string">"Item 2"</span> },
    { id: <span class="hljs-number">3</span>, text: <span class="hljs-string">"Item 3"</span> },
  ]);

  <span class="hljs-keyword">const</span> handleOnChange = <span class="hljs-function">(<span class="hljs-params">
    items: Item[],
    changedItem?: Item,
    oldIndex?: <span class="hljs-built_in">number</span>,
    newIndex?: <span class="hljs-built_in">number</span>
  </span>) =&gt;</span> {
    <span class="hljs-comment">// Update your state here.  This example assumes you know which list changed.</span>
    <span class="hljs-comment">// In a real app, you'd likely need to identify the list.</span>

    setItems([...items]);
  };

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;Sortable
        items={items}
        onChange={handleOnChange}
      &gt;
        {items.map((item: Item): JSX.Element =&gt; {
          <span class="hljs-keyword">return</span> (
            &lt;div key={item.id} className=<span class="hljs-string">"list-item"</span>&gt;
              {item.text}
            &lt;/div&gt;
          );
        })}
      &lt;/Sortable&gt;
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SimpleList;
</code></pre>
<p>As you can see, the API is straightforward:</p>
<ul>
<li><p>Pass your list of items to the <code>items</code> prop.</p>
</li>
<li><p>Provide a <code>onChange</code> callback to update the order of the items.</p>
</li>
<li><p>Render your items as children of the <code>Sortable</code> component.</p>
</li>
</ul>
<h2 id="heading-installation"><strong>Installation</strong></h2>
<p>To install <code>@spfxappdev/sortable</code>, simply run:</p>
<pre><code class="lang-bash"> npm install @spfxappdev/sortable
</code></pre>
<h2 id="heading-when-to-use-spfxappdevsortable"><strong>When to Use</strong> <code>@spfxappdev/sortable</code></h2>
<p><code>@spfxappdev/sortable</code> is ideal for cases where:</p>
<ul>
<li><p>You need basic drag-and-drop sorting.</p>
</li>
<li><p>You want to keep your bundle size low.</p>
</li>
<li><p>You prefer a simple and easy-to-use API.</p>
</li>
<li><p>You don't need fancy animations or complex features.</p>
</li>
</ul>
<h2 id="heading-when-to-look-elsewhere"><strong>When to Look Elsewhere</strong></h2>
<p>If your project needs advanced features like:</p>
<ul>
<li><p>Complex animations.</p>
</li>
<li><p>more complex drag and drop behaviors.</p>
</li>
</ul>
<p>Then you may want to look for a more detailed library.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p><code>@spfxappdev/sortable</code> shows the power of simplicity. It's a lightweight and easy-to-use solution for basic drag-and-drop sorting in React. If you're tired of big libraries and just want a simple way to reorder your list items, give <code>@spfxappdev/sortable</code> a try.</p>
<h2 id="heading-demo">Demo</h2>
<p>In the <a target="_blank" href="https://github.com/SPFxAppDev/react-sortable">GitHub project repository</a> in the “<a target="_blank" href="https://github.com/SPFxAppDev/react-sortable/tree/master/samples">samples</a>” folder you will find some examples that you can run locally.</p>
<p>Additionally you can find samples on <a target="_blank" href="https://codesandbox.io/p/sandbox/sortable-examples-5sgrqq"><strong>codesandbox</strong></a></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/5sgrqq?view=preview">https://codesandbox.io/embed/5sgrqq?view=preview</a></div>
]]></content:encoded></item><item><title><![CDATA[How to check whether the scheduling page feature is enabled in SPFx]]></title><description><![CDATA[In my current project, I have to find out if the "Schedule" feature for communication pages is turned on. Honestly, I'm not sure if my method, which I'm sharing here, is the correct or supported way because I haven't seen any other methods online. If...]]></description><link>https://spfx-app.dev/how-to-check-whether-the-scheduling-page-feature-is-enabled-in-spfx</link><guid isPermaLink="true">https://spfx-app.dev/how-to-check-whether-the-scheduling-page-feature-is-enabled-in-spfx</guid><category><![CDATA[sPfx]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[SharePointFramework]]></category><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Fri, 12 Apr 2024 18:11:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712945437576/71ce6282-4a5a-46c2-ada8-4835fc070c5a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my current project, I have to find out if the "Schedule" feature for communication pages is turned on. Honestly, I'm not sure if my method, which I'm sharing here, is the correct or supported way because I haven't seen any other methods online. If you know a better way or if my method is wrong, please leave a comment.</p>
<p>As you know, you have the property <code>this.context.pageContext</code> in SPFx. You also know the <code>legacyContext</code> property in the <code>pageContext</code> object. The name might be scary and probably many do not use this property because of the name. But there is a lot of information right there. And whether certain features are activated or not, among other things.</p>
<p>However, if you want to know whether the Scheduling Feature is activated or not, you can check this as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-built_in">this</span>.context.pageContext.legacyPageContext.featureInfo.SitePagesScheduling.Enabled
</code></pre>
<p>BTW: The <code>featureInfo</code> contains a lot more information about other features</p>
<p>Happy Coding ;)</p>
]]></content:encoded></item><item><title><![CDATA[Fixing Errors with SPFx Services in Application Customizer During Debugging]]></title><description><![CDATA[Before you deploy your application, you usually want to debug or test it. During development, you need to add
?loadSPFX=true&debugManifestsFile=https%3A%2F%2Flocalhost%3A4321%2Ftemp%2Fmanifests.js

to the URL to test your solution. If you're working ...]]></description><link>https://spfx-app.dev/fixing-errors-with-spfx-services-in-application-customizer-during-debugging</link><guid isPermaLink="true">https://spfx-app.dev/fixing-errors-with-spfx-services-in-application-customizer-during-debugging</guid><category><![CDATA[sPfx]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[SharePoint Online]]></category><category><![CDATA[SharePointFramework]]></category><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Fri, 05 Apr 2024 07:55:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712303605528/2e144df4-274e-4b55-b684-262c33c4e962.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Before you deploy your application, you usually want to debug or test it. During development, you need to add</p>
<pre><code class="lang-xml">?loadSPFX=true&amp;debugManifestsFile=https%3A%2F%2Flocalhost%3A4321%2Ftemp%2Fmanifests.js
</code></pre>
<p>to the URL to test your solution. If you're working on an Application Customizer and the Solution isn't installed yet, you need to add a bit more to the URL:</p>
<pre><code class="lang-xml">?loadSPFX=true&amp;debugManifestsFile=https://localhost:4321/temp/manifests.js&amp;customActions={"yourAppIDFrom_manifest.json":{"location":"ClientSideExtension.ApplicationCustomizer"}}
</code></pre>
<blockquote>
<p>Note: You should replace <a target="_blank" href="https://localhost:4321/temp/manifests.js&amp;customActions=%7B%22yourAppIDFrom_manifest.json%22:%7B%22location%22:%22ClientSideExtension.ApplicationCustomizer"><code>yourAppIDFrom_manifest.json</code></a> in the URL with your own ID.</p>
</blockquote>
<p>This method usually works well. However, if you use a service class in your Extension (<code>this.context.serviceScope.consume(...)</code>) and begin debugging as mentioned, you will see an error message in the console.</p>
<p><code>Error: Failed to create application customizer 'ClientSideExtension.ApplicationCustomizer.YOUR_APP_ID'</code></p>
<p>and the information:</p>
<p><code>Extension failed to load for componentId "YOUR_APP_ID"</code></p>
<h2 id="heading-how-to-fix-this-error">How to fix this error</h2>
<p>My colleague asked me if I could help him with the error. We found out that the code was not loaded, not even the constructor. Initially, I suspected a problem with how the service was set up. However, even after making a new, empty service class, the error persisted. Then, I decided to add the app to the app catalog and install it. That solved the problem!</p>
<p>I'm not sure why this happens. It's different from web parts, where services work fine during debugging without needing to install anything.</p>
<p>PS: Once the Application Customizer is installed, you don't need the long URL query with the <code>customActions</code> parameter anymore. Otherwise, it will load twice. After installation, this is all you need:</p>
<pre><code class="lang-xml">?loadSPFX=true&amp;debugManifestsFile=https%3A%2F%2Flocalhost%3A4321%2Ftemp%2Fmanifests.js
</code></pre>
<p>Happy Coding ;)</p>
]]></content:encoded></item><item><title><![CDATA[How to create a Microsoft Fluent UI Searchable Dropdown Component with react]]></title><description><![CDATA[The Fluent UI dropdown component provided by Microsoft is good. But unfortunately it does not support to "search" for items from the list. In this post, I will describe how to create a custom React component with the ability to filter the dropdown co...]]></description><link>https://spfx-app.dev/how-to-create-a-microsoft-fluent-ui-searchable-dropdown-component-with-react</link><guid isPermaLink="true">https://spfx-app.dev/how-to-create-a-microsoft-fluent-ui-searchable-dropdown-component-with-react</guid><category><![CDATA[fluentui]]></category><category><![CDATA[fluent ui]]></category><category><![CDATA[sPfx]]></category><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Fri, 01 Dec 2023 12:24:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701433411892/c755c61a-d41f-4db5-afdc-e02ad82fd4fb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The <a target="_blank" href="https://developer.microsoft.com/en-us/fluentui#/controls/web/dropdown">Fluent UI dropdown component</a> provided by Microsoft is good. But unfortunately it does not support to "search" for items from the list. In this post, I will describe how to create a custom React component with the ability to filter the dropdown components.</p>
<p>The user should be able to search/filter the list in the dropdown component. You can fulfill this requirement with a custom React component.</p>
<p>Just create a new react component. The Properties are inherited from <code>IDropdownProps</code> but with two additional properties.</p>
<pre><code class="lang-typescript">onSearchValueChanged(searchValue: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">void</span>;
searchboxProps?: Omit&lt;ISearchBoxProps, <span class="hljs-string">'onChange'</span> | <span class="hljs-string">'onClear'</span> | <span class="hljs-string">'onSearch'</span>&gt;;
</code></pre>
<p>The <code>onSearchValueChanged</code> property is required because you need to handle the event when a user enters something in the search field (= You want to filter the list according to the search term).</p>
<p>Why I exclude the <code>onChange</code> , <code>onClear</code> and <code>onSearch</code> for <code>searchboxProps</code> ?</p>
<p>I think all three properties should deal with the same event: the user searching for something. This is the reason why the <code>onSearchValueChanged</code> property is required. As a developer, you should be able to determine how the list should be filtered. In my opinion, the <code>onChange</code>, <code>onClear</code>, and <code>onSearch</code> properties (from the <a target="_blank" href="https://developer.microsoft.com/en-us/fluentui#/controls/web/searchbox">Fluent UI SearchBox Component</a>) should trigger the event with a single event. This is the reason why I created the <code>onSearchValueChanged</code> property. This event is automatically triggered on <code>onChange</code>, <code>onClear</code>, and <code>onSearch</code>.</p>
<p>Use <code>searchboxProps</code> to set all search box properties, except <code>onChange</code>, <code>onClear</code>, and <code>onSearch</code>. The reason was explained earlier. These events are not needed anymore. Use <code>onSearchValueChanged</code> instead.</p>
<h2 id="heading-the-render-method">The <code>render</code> method</h2>
<p>The content of the <code>render</code> method is really simple and short. It is enough to render the default <code>Dropdown</code> component of Fluent UI and pass all the properties of the component to the control (since the properties inherit from <code>IDropdownProps</code>). Here is the code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">public</span> render(): React.ReactElement&lt;ISearchableDropDownProps&gt; {
    <span class="hljs-keyword">return</span> (
      &lt;Dropdown
        {...this.props}
        options={<span class="hljs-built_in">this</span>.getOptions()}
        onRenderOption={(
          option?: ISelectableOption,
          defaultRender?: <span class="hljs-function">(<span class="hljs-params">props?: ISelectableOption</span>) =&gt;</span> JSX.Element | <span class="hljs-literal">null</span>,
        ): JSX.Element | <span class="hljs-function"><span class="hljs-params">null</span> =&gt;</span> {
          <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.onRenderOption(option, defaultRender);
        }}
      /&gt;
    );
  }
</code></pre>
<p>As you can see, I am overriding the <code>onRenderOption</code> method and the <code>options</code> property. I'll describe why I'm doing this. First, let's take a look at the <code>getOptions</code> method</p>
<h2 id="heading-the-getoptions-method">The <code>getOptions</code> method</h2>
<pre><code class="lang-typescript"><span class="hljs-keyword">private</span> getOptions(): IDropdownOption[] {
    <span class="hljs-keyword">const</span> result: IDropdownOption[] = [];

    result.push({
      key: <span class="hljs-string">"search"</span>,
      text: <span class="hljs-string">""</span>,
      itemType: SelectableOptionMenuItemType.Header,
    });

    <span class="hljs-keyword">return</span> result.concat([...this.props.options]);
  }
</code></pre>
<p>I think it's self-explanatory. We add a "dummy" option with the key "search" as the option header. Then, the original options are merged.</p>
<h2 id="heading-the-onrenderoption-method">The <code>onRenderOption</code> method</h2>
<p>Now, let's take a look at what the onRenderOption looks like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">private</span> onRenderOption(
    option?: ISelectableOption,
    defaultRender?: <span class="hljs-function">(<span class="hljs-params">props?: ISelectableOption</span>) =&gt;</span> JSX.Element | <span class="hljs-literal">null</span>,
  ): JSX.Element | <span class="hljs-literal">null</span> {
    <span class="hljs-keyword">if</span> (!option) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

    <span class="hljs-keyword">if</span> (
      option.itemType === SelectableOptionMenuItemType.Header &amp;&amp;
      option.key === <span class="hljs-string">"search"</span>
    ) {
      <span class="hljs-keyword">return</span> (
        &lt;SearchBox
          {...this.props.searchboxProps}
          onChange={(
            ev?: React.ChangeEvent&lt;HTMLInputElement&gt;,
            newValue?: <span class="hljs-built_in">string</span>,
          ): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">this</span>.props.onSearchValueChanged === <span class="hljs-string">"function"</span>) {
              <span class="hljs-built_in">this</span>.props.onSearchValueChanged(newValue || <span class="hljs-string">""</span>);
            }
          }}
          onSearch={(newValue: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">this</span>.props.onSearchValueChanged === <span class="hljs-string">"function"</span>) {
              <span class="hljs-built_in">this</span>.props.onSearchValueChanged(newValue);
            }
          }}
          onClear={<span class="hljs-function">() =&gt;</span> {
            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">this</span>.props.onSearchValueChanged === <span class="hljs-string">"function"</span>) {
              <span class="hljs-built_in">this</span>.props.onSearchValueChanged(<span class="hljs-string">""</span>);
            }
          }}
        /&gt;
      );
    }

    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">this</span>.props.onRenderOption === <span class="hljs-string">"function"</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.props.onRenderOption(option, defaultRender);
    }

    <span class="hljs-keyword">if</span> (!defaultRender) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

    <span class="hljs-keyword">return</span> defaultRender(option);
  }
</code></pre>
<p>Okay, this method is a bit longer, but not too difficult to understand. We check if the current option element (<code>ISelectableOption</code>) is of type "Header" and the key is "search". If so, we add the FluentUI <code>SearchBox</code> component and pass all <code>searchboxProps</code> properties to the search box. Then, we override the <code>onChange</code>, <code>onSearch</code>, and <code>onClear</code> methods. This allows us to trigger our own <code>onSearchValueChanged</code> function (see above).</p>
<p>Otherwise, if it is not our search option, we call the custom <code>onRenderOption</code> method (= Your own defined render method) if this property is defined, or execute the <code>defaultRender</code> method.</p>
<p>That's it. The component is ready for use. Here is the complete code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {
  Dropdown,
  IDropdownProps,
  IDropdownOption,
  SelectableOptionMenuItemType,
  ISelectableOption,
  SearchBox,
  ISearchBoxProps,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@fluentui/react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> ISearchableDropDownProps <span class="hljs-keyword">extends</span> IDropdownProps {
  onSearchValueChanged(searchValue: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">void</span>;
  searchboxProps?: Omit&lt;ISearchBoxProps, <span class="hljs-string">"onChange"</span> | <span class="hljs-string">"onClear"</span> | <span class="hljs-string">"onSearch"</span>&gt;;
}

<span class="hljs-keyword">interface</span> ISearchableDropDownState {}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> SearchableDropDown <span class="hljs-keyword">extends</span> React.Component&lt;
  ISearchableDropDownProps,
  ISearchableDropDownState
&gt; {
  <span class="hljs-keyword">public</span> state: ISearchableDropDownState = {};

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> defaultProps: Partial&lt;ISearchableDropDownProps&gt; = {
    searchboxProps: {
      autoComplete: <span class="hljs-string">"false"</span>,
      autoFocus: <span class="hljs-literal">true</span>,
    },
  };

  <span class="hljs-keyword">public</span> render(): React.ReactElement&lt;ISearchableDropDownProps&gt; {
    <span class="hljs-keyword">return</span> (
      &lt;Dropdown
        {...this.props}
        options={<span class="hljs-built_in">this</span>.getOptions()}
        onRenderOption={(
          option?: ISelectableOption,
          defaultRender?: <span class="hljs-function">(<span class="hljs-params">props?: ISelectableOption</span>) =&gt;</span> JSX.Element | <span class="hljs-literal">null</span>,
        ): JSX.Element | <span class="hljs-function"><span class="hljs-params">null</span> =&gt;</span> {
          <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.onRenderOption(option, defaultRender);
        }}
      /&gt;
    );
  }

  <span class="hljs-keyword">private</span> onRenderOption(
    option?: ISelectableOption,
    defaultRender?: <span class="hljs-function">(<span class="hljs-params">props?: ISelectableOption</span>) =&gt;</span> JSX.Element | <span class="hljs-literal">null</span>,
  ): JSX.Element | <span class="hljs-literal">null</span> {
    <span class="hljs-keyword">if</span> (!option) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

    <span class="hljs-keyword">if</span> (
      option.itemType === SelectableOptionMenuItemType.Header &amp;&amp;
      option.key === <span class="hljs-string">"search"</span>
    ) {
      <span class="hljs-keyword">return</span> (
        &lt;SearchBox
          {...this.props.searchboxProps}
          onChange={(
            ev?: React.ChangeEvent&lt;HTMLInputElement&gt;,
            newValue?: <span class="hljs-built_in">string</span>,
          ): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">this</span>.props.onSearchValueChanged === <span class="hljs-string">"function"</span>) {
              <span class="hljs-built_in">this</span>.props.onSearchValueChanged(newValue || <span class="hljs-string">""</span>);
            }
          }}
          onSearch={(newValue: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">this</span>.props.onSearchValueChanged === <span class="hljs-string">"function"</span>) {
              <span class="hljs-built_in">this</span>.props.onSearchValueChanged(newValue);
            }
          }}
          onClear={<span class="hljs-function">() =&gt;</span> {
            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">this</span>.props.onSearchValueChanged === <span class="hljs-string">"function"</span>) {
              <span class="hljs-built_in">this</span>.props.onSearchValueChanged(<span class="hljs-string">""</span>);
            }
          }}
        /&gt;
      );
    }

    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-built_in">this</span>.props.onRenderOption === <span class="hljs-string">"function"</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.props.onRenderOption(option, defaultRender);
    }

    <span class="hljs-keyword">if</span> (!defaultRender) {
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

    <span class="hljs-keyword">return</span> defaultRender(option);
  }

  <span class="hljs-keyword">private</span> getOptions(): IDropdownOption[] {
    <span class="hljs-keyword">const</span> result: IDropdownOption[] = [];

    result.push({
      key: <span class="hljs-string">"search"</span>,
      text: <span class="hljs-string">""</span>,
      itemType: SelectableOptionMenuItemType.Header,
    });

    <span class="hljs-keyword">return</span> result.concat([...this.props.options]);
  }
}
</code></pre>
<h2 id="heading-how-to-use-it">How to use it</h2>
<p>You can use the component like the FluentUI <code>Dropdown</code> component, but with two relevant exceptions. First, you need to take care of filtering the options when a user searches for something, or set the initial values when the search is finished or something is selected. Second, you should definitely set the <code>defaultSelectedKey(s)</code> or <code>selectedKey(s)</code> property; otherwise, the "selected" element(s) will be changed when the user searches for an element.</p>
<h3 id="heading-example">Example</h3>
<pre><code class="lang-typescript">&lt;SearchableDropDown
    defaultSelectedKey={<span class="hljs-built_in">this</span>.state.selectedKeys}
    onChange={<span class="hljs-function">(<span class="hljs-params">ev: <span class="hljs-built_in">any</span>, option</span>) =&gt;</span> {
            <span class="hljs-built_in">this</span>.setState({
              selectedKeys: option ? option.key.toString() : <span class="hljs-string">""</span>,
              filteredItems: [...initialItems],
            });
    }}
    onSearchValueChanged={<span class="hljs-function">(<span class="hljs-params">searchValue: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
            <span class="hljs-keyword">const</span> newOptions = <span class="hljs-built_in">this</span>.onDropDownSearch(
              searchValue,
              initialItems,
            );
            <span class="hljs-built_in">this</span>.setState({
              filteredItems: newOptions,
            });
          }}
    options={<span class="hljs-built_in">this</span>.state.filteredItems}
/&gt;
</code></pre>
<p>The <code>onDropDownSearch</code> method looks like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">private</span> onDropDownSearch(
    searchValue: <span class="hljs-built_in">string</span>,
    initialValues: IDropdownOption[],
  ): IDropdownOption[] {
    <span class="hljs-keyword">if</span> (isNullOrEmpty(searchValue)) {
      <span class="hljs-keyword">return</span> [...initialValues];
    }

    <span class="hljs-comment">//OR FIlter but whatever you want...</span>
    <span class="hljs-keyword">const</span> filteredOptions = [...initialValues].Where(
      <span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span>
        i.text.Contains(searchValue) &amp;&amp;
        (!isset(i.itemType) || i.itemType === DropdownMenuItemType.Normal),
    );

    <span class="hljs-keyword">return</span> filteredOptions;
  }
</code></pre>
<p>BTW: In this example, I use my <a target="_blank" href="https://spfxappdev.github.io/ts-utility/">@spfxappdev/utility</a> package to filter (<a target="_blank" href="https://spfxappdev.github.io/ts-utility/#where">Where</a>) and to check if the search value is empty and set (<a target="_blank" href="https://spfxappdev.github.io/ts-utility/#isnullorempty">isNullOrEmpty</a>).</p>
<h2 id="heading-sandbox-demo">Sandbox / Demo</h2>
<p>You can try out my solution in my <a target="_blank" href="https://jhfyv9.csb.app">Codesandbox demo</a> (or <a target="_blank" href="https://yj5jwh.csb.app/">here</a> with many options/items). Feel free to copy/modify the code (maybe you can write a comment about what was changed 😊)</p>
<p>What do you think? How do you like it? I would be happy about feedback.</p>
<p>Happy coding ;)</p>
]]></content:encoded></item><item><title><![CDATA[SPFx SharePoint Development: Quickly Generate List-Based Models Using a Single Command]]></title><description><![CDATA[In a recent post, I shared my CLI @spfxappdev/cli which has some awesome features. Many of these features come from my other posts, like "My personal tips how to configure a SPFx project after creation"
Today I published a new version of the CLI, v1....]]></description><link>https://spfx-app.dev/spfx-sharepoint-development-quickly-generate-list-based-models-using-a-single-command</link><guid isPermaLink="true">https://spfx-app.dev/spfx-sharepoint-development-quickly-generate-list-based-models-using-a-single-command</guid><category><![CDATA[sPfx]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[SharePoint Online]]></category><category><![CDATA[cli]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Thu, 07 Sep 2023 16:47:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694105117470/ee41b7fb-2e9e-4bad-a807-7818ffed36c9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a recent post, <a target="_blank" href="https://spfx-app.dev/my-cli-for-spfx-development">I shared my CLI</a> <code>@spfxappdev/cli</code> which has some awesome features. Many of these features come from my other posts, like "<a target="_blank" href="https://spfx-app.dev/my-personal-tips-how-to-configure-a-spfx-project-after-creation">My personal tips how to configure a SPFx project after creation</a>"</p>
<p><a target="_blank" href="https://www.npmjs.com/package/@spfxappdev/cli">Today I published a new version of the CLI, v1.1.2.</a> This version comes with a new feature. It will help you to save a lot of time.</p>
<p>I guess almost everyone who works with SharePoint and SharePoint API has the same challenge. You need to aggregate a list and the items in it. So far so good, but sometimes the (internal) field names are, shall we say, "unfriendly". For example, if you create a new field via the UI and choose a display name like <code>My Fancy Column</code>, SharePoint generates the internal/static name and sets it to <code>My_x0020_Fancy_x0020_Column</code>  </p>
<p>To get the value from the list item (via SharePoint REST API), you need to do something like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> myModel = <span class="hljs-keyword">new</span> MyModel();
<span class="hljs-keyword">const</span> listItem = listItemCollection[i];
myModel.myFancyColumn = listItem[<span class="hljs-string">'My_x0020_Fancy_x0020_Column'</span>];
</code></pre>
<p>And that's exactly what can be done faster and easier with the CLI in just a few seconds. Let's demonstrate it using the standard <code>SitePages</code> library.</p>
<pre><code class="lang-bash"><span class="hljs-comment">#Long</span>
spfxappdev generate model Page --weburl https://{tenant}.sharepoint.com --username <span class="hljs-string">"your@email.com"</span> --password <span class="hljs-string">"yourPW"</span> --list SitePages
<span class="hljs-comment">#Short (with alias)</span>
spfx g m Page --u https://{tenant}.sharepoint.com --user <span class="hljs-string">"your@email.com"</span> --p <span class="hljs-string">"yourPW"</span> --l SitePages
</code></pre>
<p>The generated interface is:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> IPage {
    name: <span class="hljs-built_in">string</span>;
    complianceAssetId: <span class="hljs-built_in">string</span>;
    wikiContent: <span class="hljs-built_in">string</span>;
    title: <span class="hljs-built_in">string</span>;
    authoringCanvasContent: <span class="hljs-built_in">any</span>;
    bannerImageURL: UrlFieldValue;
    description: <span class="hljs-built_in">string</span>;
    promotedState: <span class="hljs-built_in">number</span>;
    firstPublishedDate: <span class="hljs-built_in">Date</span>;
    pageLayoutContent: <span class="hljs-built_in">any</span>;
    authorBylineId: <span class="hljs-built_in">number</span>[];
    topicHeader: <span class="hljs-built_in">string</span>;
    sitePageFlags: <span class="hljs-built_in">string</span>[];
    callToAction: <span class="hljs-built_in">string</span>;
    originalSourceUrl: <span class="hljs-built_in">string</span>;
    originalSourceSiteID: <span class="hljs-built_in">string</span>;
    originalSourceWebID: <span class="hljs-built_in">string</span>;
    originalSourceListID: <span class="hljs-built_in">string</span>;
    originalSourceItemID: <span class="hljs-built_in">string</span>;
    id: <span class="hljs-built_in">number</span>;
    contentType: <span class="hljs-built_in">string</span>;
    created: <span class="hljs-built_in">Date</span>;
    createdById: <span class="hljs-built_in">number</span>;
    modified: <span class="hljs-built_in">Date</span>;
    modifiedById: <span class="hljs-built_in">number</span>;
    copySource: <span class="hljs-built_in">string</span>;
    checkedOutToId: <span class="hljs-built_in">number</span>;
    checkInCommentId: <span class="hljs-built_in">number</span>;
    <span class="hljs-keyword">type</span>: <span class="hljs-built_in">string</span>;
    fileSize: <span class="hljs-built_in">string</span>;
    itemChildCountId: <span class="hljs-built_in">number</span>;
    folderChildCountId: <span class="hljs-built_in">number</span>;
    commentCountId: <span class="hljs-built_in">number</span>;
    likeCountId: <span class="hljs-built_in">number</span>;
    sensitivityId: <span class="hljs-built_in">number</span>;
    edit: <span class="hljs-built_in">string</span>;
    sourceVersionConvertedDocumentId: <span class="hljs-built_in">number</span>;
    sourceNameConvertedDocumentId: <span class="hljs-built_in">number</span>;
}
</code></pre>
<p>The generated class looks like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { IPage } <span class="hljs-keyword">from</span> <span class="hljs-string">'./'</span>;
<span class="hljs-keyword">import</span> { mapper } <span class="hljs-keyword">from</span> <span class="hljs-string">'@spfxappdev/mapper'</span>;
<span class="hljs-keyword">import</span> { UrlFieldValue } <span class="hljs-keyword">from</span> <span class="hljs-string">'./'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Page <span class="hljs-keyword">implements</span> IPage {
    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'FileLeafRef'</span>,  })
    <span class="hljs-keyword">public</span> name: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'ComplianceAssetId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> complianceAssetId: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'WikiField'</span>,  })
    <span class="hljs-keyword">public</span> wikiContent: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'Title'</span>,  })
    <span class="hljs-keyword">public</span> title: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'CanvasContent1'</span>,  })
    <span class="hljs-keyword">public</span> authoringCanvasContent: <span class="hljs-built_in">any</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'BannerImageUrl'</span>, <span class="hljs-keyword">type</span>: UrlFieldValue,  })
    <span class="hljs-keyword">public</span> bannerImageURL: UrlFieldValue;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'Description'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> description: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'PromotedState'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> promotedState: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'FirstPublishedDate'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-built_in">Date</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> firstPublishedDate: <span class="hljs-built_in">Date</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'LayoutWebpartsContent'</span>,  })
    <span class="hljs-keyword">public</span> pageLayoutContent: <span class="hljs-built_in">any</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__AuthorBylineId.results'</span>,  })
    <span class="hljs-keyword">public</span> authorBylineId: <span class="hljs-built_in">number</span>[];

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__TopicHeader'</span>,  })
    <span class="hljs-keyword">public</span> topicHeader: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__SPSitePageFlags.results'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> sitePageFlags: <span class="hljs-built_in">string</span>[];

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__SPCallToAction'</span>,  })
    <span class="hljs-keyword">public</span> callToAction: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__OriginalSourceUrl'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> originalSourceUrl: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__OriginalSourceSiteId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> originalSourceSiteID: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__OriginalSourceWebId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> originalSourceWebID: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__OriginalSourceListId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> originalSourceListID: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__OriginalSourceItemId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> originalSourceItemID: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'ID'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> id: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'ContentType'</span>,  })
    <span class="hljs-keyword">public</span> contentType: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'Created'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-built_in">Date</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> created: <span class="hljs-built_in">Date</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'AuthorId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> createdById: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'Modified'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-built_in">Date</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> modified: <span class="hljs-built_in">Date</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'EditorId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> modifiedById: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__CopySource'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> copySource: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'CheckoutUserId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> checkedOutToId: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__CheckinCommentId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> checkInCommentId: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'DocIcon'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">type</span>: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'FileSizeDisplay'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> fileSize: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'ItemChildCountId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> itemChildCountId: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'FolderChildCountId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> folderChildCountId: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__CommentCountId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> commentCountId: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__LikeCountId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> likeCountId: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'OData__DisplayNameId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> sensitivityId: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'Edit'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> edit: <span class="hljs-built_in">string</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'ParentVersionStringId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> sourceVersionConvertedDocumentId: <span class="hljs-built_in">number</span>;

    <span class="hljs-meta">@mapper</span>({ nameOrPath: <span class="hljs-string">'ParentLeafNameId'</span>, toClassOnly: <span class="hljs-literal">true</span> })
    <span class="hljs-keyword">public</span> sourceNameConvertedDocumentId: <span class="hljs-built_in">number</span>;    
}
</code></pre>
<p>As you can see, the CLI generates the class using the <code>@mapper</code> decorator. This decorator is a part of my <a target="_blank" href="https://www.npmjs.com/package/@spfxappdev/mapper">npm package</a>. It is developed to map a model to a SharePoint API result and vice versa. Another point you may have noticed is that the internal names are stored in the <code>nameOrPath</code> property, but the model property is more the general name specification in <code>camelCase</code>.</p>
<p>But how you can "convert" the API Result to your model and back to the SP List Model? It is very easy:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { toClass, toPlain  } <span class="hljs-keyword">from</span> <span class="hljs-string">'@spfxappdev/mapper'</span>;
<span class="hljs-keyword">import</span> { Page } <span class="hljs-keyword">from</span> <span class="hljs-string">'@src/models'</span>;

<span class="hljs-keyword">class</span> MyService {

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> getPages(): <span class="hljs-built_in">Promise</span>&lt;IPage[]&gt; {
        <span class="hljs-comment">//const pageCollection = await ...Your API call</span>
        <span class="hljs-keyword">const</span> pages: Page[] = toClass(Page, pageCollection);
        <span class="hljs-keyword">return</span> pages;
    }

    <span class="hljs-keyword">public</span> updatePage(page: IPage): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">const</span> spData = toPlain(page);

        <span class="hljs-comment">//YOUR REST API CALL TO UPDATE THE PAGE</span>
    }
}
</code></pre>
<p>The <code>toPlain</code> method ignores properties labeled as <code>toClassOnly: true</code> because they are read-only fields, like Editor/Author, and can't be updated.</p>
<p>That is great, isn't it?</p>
<p>By the way, you don't have to pass all the values like <code>--weburl</code>, <code>--username</code> and <code>--password</code> every time. You can set the values once via the <code>config set</code> command and then these values will be used (the password is encrypted).</p>
<p>Also new in this version: You can now <a target="_blank" href="https://github.com/SPFxAppDev/spfxcli#spfxappdev-config-create">create local configuration files</a>. So you can have different configurations per project.</p>
<p>What do you think? How do you like it? I would be happy about feedback.</p>
<p>Happy coding ;)</p>
]]></content:encoded></item><item><title><![CDATA[Understanding package.json versioning]]></title><description><![CDATA[If you are a developer who works with npm, you probably have seen the package.json file in your project. This file contains information about your project and its dependencies. One of the most important fields in the package.json file is the version ...]]></description><link>https://spfx-app.dev/understanding-packagejson-versioning</link><guid isPermaLink="true">https://spfx-app.dev/understanding-packagejson-versioning</guid><category><![CDATA[npm]]></category><category><![CDATA[versioning]]></category><category><![CDATA[packaging]]></category><category><![CDATA[package manager]]></category><category><![CDATA[package]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Wed, 06 Sep 2023 07:27:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693985157116/015515a1-ad2e-4ff8-96d3-4e52bb020eb9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you are a developer who works with <a target="_blank" href="https://www.npmjs.com/">npm</a>, you probably have seen the <code>package.json</code> file in your project. This file contains information about your project and its dependencies. One of the most important fields in the package.json file is the version field, which specifies the version of your project. But how do you choose a version number for your project? And what do the different symbols mean when you specify the versions of your dependencies?</p>
<p>In this blog post, I will explain how to use <a target="_blank" href="https://semver.org/">semantic versioning</a> for your project and how to understand the version ranges of your dependencies.</p>
<h2 id="heading-semantic-versioning">Semantic versioning</h2>
<p>Semantic versioning is a convention for assigning meaningful version numbers to software projects. It follows the format of <code>MAJOR.MINOR.PATCH</code>, where:</p>
<ul>
<li><p><code>MAJOR</code> is incremented when you make incompatible API changes</p>
</li>
<li><p><code>MINOR</code> is incremented when you add functionality in a backward-compatible manner</p>
</li>
<li><p><code>PATCH</code> is incremented when you make backward-compatible bug fixes</p>
</li>
</ul>
<p>For example, if you have a project with the version <code>1.2.3</code> and you add a new feature without breaking any existing functionality, you can increase the <code>MINOR</code> part and make it <code>1.3.0</code>. If you fix a bug without changing the API, you can increase the <code>PATCH</code> part and make it <code>1.3.1</code>. If you change the API in a way that breaks existing code, you can increase the <code>MAJOR</code> part and make it <code>2.0.0</code>.</p>
<p>Semantic versioning helps you communicate the changes in your project and avoid compatibility issues with other projects that depend on yours.</p>
<h2 id="heading-version-ranges">Version ranges</h2>
<p>When you specify the versions of your dependencies in the <code>package.json</code> file, you can use different symbols to indicate the range of acceptable versions. These symbols are:</p>
<ul>
<li><p><code>~</code> (tilde): This means that you accept any <code>patch</code> updates within the same <code>minor</code> version. For example, <code>~1.4.0</code> means that you accept any version from <code>1.4.0</code> to <code>1.4.x</code>, but not <code>1.5.x</code> or higher.</p>
</li>
<li><p><code>^</code> (caret): This means that you accept any <code>minor</code> or <code>patch</code> updates within the same major version. For example, <code>^1.4.0</code> means that you accept any version from <code>1.4.0</code> to <code>1.x.x</code>, but not <code>2.x.x</code> or higher.</p>
</li>
<li><p><code>*</code> (asterisk): This means that you accept any version of the dependency. This is <strong>not recommended</strong> as it can introduce breaking changes or bugs without your control.</p>
</li>
<li><p><code>&gt;</code> (greater than), <code>&lt;</code> (less than), <code>&gt;=</code> (greater than or equal to), <code>&lt;=</code> (less than or equal to): These symbols allow you to specify a range of versions using inequality operators. For example, <code>&gt;=1.4.0 &lt;2.0.0</code> means that you accept any version from <code>1.4.0</code> to <code>1.x.x</code>, but not <code>2.x.x</code> or higher.</p>
</li>
<li><p><code>-</code> (hyphen): This allows you to specify a range of versions using a dash. For example, <code>1.4.0 - 2.0.0</code> means that you accept any version from <code>1.4.0</code> to <code>2.0.0</code> inclusive.</p>
</li>
<li><p><code>||</code> (double pipe): This allows you to combine multiple ranges using an OR operator. For example, <code>^1.4.0 || ^2.0.0</code> means that you accept any version from <code>1.x.x</code> or <code>2.x.x</code>.</p>
</li>
</ul>
<p>When you run <code>npm install</code>, npm will look at the version ranges of your dependencies and install the latest compatible version available.</p>
<h2 id="heading-why-use-version-ranges">Why use version ranges?</h2>
<p>Using version ranges allows you to benefit from bug fixes and new features without having to update your <code>package.json</code> file every time a new version of a dependency is released.</p>
<p>However, there are also some risks involved with using version ranges:</p>
<ul>
<li><p>You may introduce breaking changes or bugs if a dependency updates its API in an incompatible way within the same major version.</p>
</li>
<li><p>You may miss important updates if a dependency releases a new <code>major</code> version with significant improvements or security fixes.</p>
</li>
<li><p>You may have inconsistent results if different developers or environments install different versions of the same dependency.</p>
</li>
</ul>
<p>To mitigate these risks, you can use tools like <code>npm audit</code> or <code>npm outdated</code> to check for vulnerabilities or outdated dependencies in your project.</p>
<p>You can also use tools like <code>npm shrinkwrap</code> or <code>npm lockfile</code>.</p>
<p>I hope this article helped you to better understand versioning in <code>npm</code></p>
<p>Happy Coding ;)</p>
]]></content:encoded></item><item><title><![CDATA[My CLI for SPFx development]]></title><description><![CDATA[In my last post, I gave some personal tips on how to configure an SPFx project after it is created. And in that post, I mentioned a command-line interface (CLI) I am working on. And now I have released the first version with full documentation.

UPDA...]]></description><link>https://spfx-app.dev/my-cli-for-spfx-development</link><guid isPermaLink="true">https://spfx-app.dev/my-cli-for-spfx-development</guid><category><![CDATA[sPfx]]></category><category><![CDATA[cli]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[SharePoint]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Mon, 28 Aug 2023 15:11:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693235469131/ab41d3e0-585b-476d-9efe-5b9c8f9e0226.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my <a target="_blank" href="https://spfx-app.dev/my-personal-tips-how-to-configure-a-spfx-project-after-creation">last post</a>, I gave some personal tips on how to configure an SPFx project after it is created. And in that post, I mentioned a command-line interface (CLI) I am working on. And now <a target="_blank" href="https://www.npmjs.com/package/@spfxappdev/cli">I have released the first version</a> with full <a target="_blank" href="https://github.com/SPFxAppDev/spfxcli#readme">documentation</a>.</p>
<blockquote>
<p>UPDATE: I introduced the CLI in the PnP community call. You can see the CLI in action <a target="_blank" href="https://www.youtube.com/watch?v=urhHe_NlYvI">here</a>.</p>
</blockquote>
<p>You are probably asking yourself, why create your own CLI at all? To be honest, the idea came up very spontaneously. I have many customers and for them, I create SPFx projects. The projects are (if not otherwise specified) always built and configured by me in the same way. But some configurations I can't (or don't want to) remember. So I "copy" the <code>gulp</code> and <code>tsconfig</code> configurations from other projects. I don't want to say that it takes a lot of time, probably a maximum of 15 minutes per project. But it sucks to copy and then paste everything etc. Besides, with time it became more and more configurations. Also, with such a CLI, I could provide a corporate standard. So that all my colleagues have the same procedure. And that's when the idea was born. A CLI should do this job for me. In addition, I have never created a CLI and wanted to expand my knowledge about it.</p>
<p>The first thing I had to figure out was how to create a CLI. Some tools can help with this like <a target="_blank" href="https://www.npmjs.com/package/commander">commander</a> or <a target="_blank" href="https://www.npmjs.com/package/yargs">yargs</a>. I chose <code>yargs</code> for no particular reason. But what should the CLI be able to do?</p>
<p>As mentioned before, I wanted to map my typical configurations. Those who follow my blog know that I often give tips and tricks around SPFx development. That's why the CLI is based on the following blog articles:</p>
<ul>
<li><p><a target="_blank" href="https://spfx-app.dev/my-personal-tips-how-to-configure-a-spfx-project-after-creation">My personal tips how to configure a SPFx project after creation</a></p>
</li>
<li><p><a target="_blank" href="https://spfx-app.dev/using-pnpm-in-spfx-projects">Using pnpm in SPFx projects</a></p>
</li>
<li><p><a target="_blank" href="https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version">Package SPFx solution with one command and automatically increase the version</a></p>
</li>
<li><p><a target="_blank" href="https://spfx-app.dev/spfx-azure-devops-pipeline-increment-version-push-to-repository-and-publish-package">SPFx Azure DevOps Pipeline: Increment version, push to repository and publish package</a></p>
</li>
</ul>
<p>I started with a single command, the <code>init</code> command. This configured everything from my <a target="_blank" href="https://spfx-app.dev/my-personal-tips-how-to-configure-a-spfx-project-after-creation">last post</a>.</p>
<ol>
<li><p>A new folder <code>@spfxappdev</code> is created in the root directory of the project</p>
</li>
<li><p>The <code>gulpfile.js</code> file is modified: Aliases are registered, and the <code>bump-version</code> task is defined and the possibility to build your solution in such a way that a warning will not cause the build process to fail.</p>
</li>
<li><p><code>tsconfig.json</code> is changed: Path <code>aliases</code> and <code>baseUrl</code> are set.</p>
</li>
<li><p><code>fast-serve/webpack.extend.js</code> is changed (if available): Aliases are registered</p>
</li>
<li><p>The <code>package.json</code> file is modified: The <code>publish</code> and <code>publish:nowarn</code> commands are defined</p>
</li>
</ol>
<p>After that I had the idea to install npm packages with the command. Mostly you use the same packages like <code>@pnp/sp</code>, then they should be installed when you call the <code>init</code> command. Of course it should be possible to disable it. And you should be able to configure the packages "globally". But how do you do that? After a little research, I found the package <a target="_blank" href="https://www.npmjs.com/package/configstore">configstore</a>.</p>
<p>And there was created another command <code>config</code> with "subcommands" to handle the configuration.</p>
<p>Because <a target="_blank" href="https://spfx-app.dev/using-pnpm-in-spfx-projects">I don't use npm by now but pnpm</a>, this had to be included as well. So I added the possibility to set the package manager, also using the <code>config</code> command.</p>
<p>And because I don't want to preset my settings, everybody should have the possibility to overwrite my templates. Therefore I have also created the possibility to specify own templates, which should be used when generating the <code>ESLint</code> and <code>TSConfig</code> files.</p>
<p>And then I had the idea that it should be possible to create a new project via the CLI. Nothing else is done than calling the <code>yo @micrsoft/sharepoint</code> command. Only that the configured package manager is passed automatically. In case of <code>pnpm</code> the following commands are also executed</p>
<pre><code class="lang-bash">pnpm config <span class="hljs-built_in">set</span> auto-install-peers <span class="hljs-literal">true</span> --location project
pnpm config <span class="hljs-built_in">set</span> shamefully-hoist <span class="hljs-literal">true</span> --location project
</code></pre>
<p>Another benefit is that you no longer have to type the long command <code>yo @micrsoft/sharepoint</code>. <a target="_blank" href="https://github.com/SPFxAppDev/spfxcli">My CLI</a> also has an alias, so you could for example enter the following command to create a new project</p>
<pre><code class="lang-bash"><span class="hljs-comment">#Long form</span>
spfxappdev new 
<span class="hljs-comment">#Short form</span>
spfx n
</code></pre>
<p>I don't want to go into detail now and explain every single command and what it is for. After all, that's why I wrote the <a target="_blank" href="https://github.com/SPFxAppDev/spfxcli#readme">documentation</a>. For example, you can also create models or services that are automatically exported to the <code>index.ts</code>.</p>
<p>To keep it short, if you want to start with my CLI. Install it once globally:</p>
<pre><code class="lang-PowerShell">npm i @spfxappdev/<span class="hljs-built_in">cli</span> <span class="hljs-literal">-g</span>
</code></pre>
<p>Once installed, you can invoke CLI commands directly from your OS command line through the <code>spfxappdev</code> executable. If you want to start directly with a new project and the default configurations, then enter the following commands</p>
<pre><code class="lang-PowerShell"><span class="hljs-comment">#create a new SPFx Project</span>
spfx new
<span class="hljs-comment">#Initialize the project (alias, bump-version, publish command)</span>
spfx init
<span class="hljs-comment">#Create custom ESLint and TSConfig rules</span>
spfx rules
</code></pre>
<p>That's it. Your project is created and configured. For more ways to use <a target="_blank" href="https://www.npmjs.com/package/@spfxappdev/cli">my CLI</a>, you should read the <a target="_blank" href="https://github.com/SPFxAppDev/spfxcli#readme">documentation</a>. I would be happy about the usage. I am looking forward to your feedback. I also like to hear suggestions for new features.</p>
<p>PS: I'm not done with the CLI yet. There will be more features to come. But currently, it is in a status that I can publish :)</p>
<p>Happy coding ;)</p>
]]></content:encoded></item><item><title><![CDATA[My personal tips how to configure a SPFx project after creation]]></title><description><![CDATA[It is very easy to create a new SPFx project and you can find a lot of posts about it. One of these posts is, of course, the one from Microsoft. But what should you do after the "empty" project is created?
I will describe what I personally do after I...]]></description><link>https://spfx-app.dev/my-personal-tips-how-to-configure-a-spfx-project-after-creation</link><guid isPermaLink="true">https://spfx-app.dev/my-personal-tips-how-to-configure-a-spfx-project-after-creation</guid><category><![CDATA[sPfx]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[m365]]></category><category><![CDATA[SharePoint]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Thu, 24 Aug 2023 10:53:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1692874332570/e1721fe9-7bbf-42a3-905e-ebdb4e1cd70a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It is very easy to create a new SPFx project and you can find a lot of posts about it. One of these posts is, of course, the one from <a target="_blank" href="https://learn.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part">Microsoft</a>. But what should you do after the "empty" project is created?</p>
<p>I will describe what <strong>I personally</strong> do after I create a new project. Some steps may not be relevant for you because you prefer a different way of working or structure your project differently. But maybe there's one or two things you didn't know yet and think it's good.</p>
<blockquote>
<p>NOTE: In the meantime, I have developed a CLI that automates the settings mentioned here and more for you. <a target="_blank" href="https://spfx-app.dev/my-cli-for-spfx-development">Read more</a></p>
</blockquote>
<h2 id="heading-spfx-fast-serve">SPFx fast serve</h2>
<p>I have often mentioned that I like the npm package <a target="_blank" href="https://github.com/s-KaiNet/spfx-fast-serve">spfx-fast-serve</a> developed by <a target="_blank" href="https://github.com/s-KaiNet">Sergei Sergeev</a>. If you don't know it, you must use it! So the first step I do after creating the project is to run the <code>spfx-fast-serve</code> command in my terminal. This is of course only possible if you have installed the package globally before. After running the command, the tool prompts you to run the <code>npm install</code> command again. Since <a target="_blank" href="https://spfx-app.dev/using-pnpm-in-spfx-projects">I switched to pnpm</a>, of course, I have to run <code>pnpm install</code></p>
<h2 id="heading-configure-path-alias">Configure (path) Alias</h2>
<p>Most use relative paths to refer to files from the solution. Here are a few examples:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">import</span> HelloWorldWebPartComponent <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/HelloWorldWebPartComponent'</span>;
<span class="hljs-keyword">import</span> { MyComponent } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../components/MyComponent'</span>;
</code></pre>
<p>Custom aliases allow you to create (mostly) shorter and more intuitive paths for your imports. This is especially beneficial in larger projects where multiple levels of folder structures can lead to unwieldy (relative) paths. But it is also helpful if you want to reuse the modules later because then you don't have to work with the relative paths anymore. Here is the same example as above, but with the alias:</p>
<pre><code class="lang-TypeScript"><span class="hljs-keyword">import</span> HelloWorldWebPartComponent <span class="hljs-keyword">from</span> <span class="hljs-string">' @webparts/helloworld/components/HelloWorldWebPartComponent'</span>;
<span class="hljs-keyword">import</span> { MyComponent } <span class="hljs-keyword">from</span> <span class="hljs-string">'@components/MyComponent'</span>;
</code></pre>
<h3 id="heading-extending-tsconfigjson-with-baseurl-and-custom-alias">Extending tsconfig.json with baseUrl and custom Alias</h3>
<p>The <code>tsconfig.json</code> file is the configuration file for TypeScript projects and provides various options to control the behavior of the TypeScript compiler. By extending this file in your SPFx solution, you can better customize the TypeScript compilation process to suit your project structure and needs.</p>
<h4 id="heading-1-adding-baseurl">1. Adding baseUrl</h4>
<p>The <code>baseUrl</code> property in <code>tsconfig.json</code> allows you to set a base directory for module resolution. This means you can define a central starting point for your imports, reducing the need for long relative paths.</p>
<p>For example, by including a <code>baseUrl</code> property pointing to your project's <code>src</code> directory, you establish a foundation for cleaner, more intuitive imports. But I personally point to <code>'.'</code>. Here is an example of how the file <code>tsconfig.json</code> looks like after the change:</p>
<pre><code class="lang-json"><span class="hljs-string">"compilerOptions"</span>: {
  <span class="hljs-attr">"baseUrl"</span>: <span class="hljs-string">"."</span>,
  <span class="hljs-comment">// Other options...</span>
}
</code></pre>
<h4 id="heading-2-defining-aliases">2. Defining Aliases</h4>
<p>To create a custom alias, add the <code>paths</code> property under <code>compilerOptions</code> in your <code>tsconfig.json</code>. This property maps aliases to their corresponding paths.</p>
<p>I always create an alias for the <code>src</code> folder (that's why my <code>baseUrl</code> points to <code>'.'</code> and not <code>'src'</code>), the <code>webparts</code> folder and the <code>components</code> folder. But of course, you can also create your own alias. This is how the <code>tsconfig.json</code> looks like after the change:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"extends"</span>: <span class="hljs-string">"./node_modules/@microsoft/rush-stack-compiler-4.5/includes/tsconfig-web.json"</span>,
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-comment">// Other options...</span>
    <span class="hljs-attr">"paths"</span>: {
      <span class="hljs-attr">"@src/*"</span>: [<span class="hljs-string">"src/*"</span>],
      <span class="hljs-attr">"@components/*"</span>: [<span class="hljs-string">"src/components/*"</span>],
      <span class="hljs-attr">"@webparts/*"</span>: [<span class="hljs-string">"src/webparts/*"</span>]
    },
    <span class="hljs-attr">"baseUrl"</span>: <span class="hljs-string">"."</span>
  },
  <span class="hljs-attr">"include"</span>: [
    <span class="hljs-string">"src/**/*.ts"</span>,
    <span class="hljs-string">"src/**/*.tsx"</span>
  ]
}
</code></pre>
<h3 id="heading-registering-custom-aliases-in-gulp">Registering custom aliases in Gulp</h3>
<p>After defining aliases in your <code>tsconfig.json</code>, you must ensure they work during the build process. This involves syncing the aliases with the build output.</p>
<p>In your <code>gulpfile.js</code>, before the <code>build.initialize(require('gulp'));</code> code part, add this:</p>
<pre><code class="lang-JavaScript">build.configureWebpack.mergeConfig({
    <span class="hljs-attr">additionalConfiguration</span>: <span class="hljs-function">(<span class="hljs-params">generatedConfiguration: any</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (!generatedConfiguration.resolve.alias) {
        generatedConfiguration.resolve.alias = {};
      }  

      <span class="hljs-comment">// webparts folder</span>
      generatedConfiguration.resolve.alias[<span class="hljs-string">'@webparts'</span>] = path.resolve(
        __dirname,
        <span class="hljs-string">'lib/webparts'</span>
      );  

      <span class="hljs-comment">// components folder</span>
      generatedConfiguration.resolve.alias[<span class="hljs-string">'@components'</span>] = path.resolve(
        __dirname,
        <span class="hljs-string">'lib/components'</span>
      );

      <span class="hljs-comment">//root src folder</span>
      generatedConfiguration.resolve.alias[<span class="hljs-string">'@src'</span>] = path.resolve(
        __dirname,
        <span class="hljs-string">'lib'</span>
      );  

      <span class="hljs-keyword">return</span> generatedConfiguration;
    }
});
</code></pre>
<p>Make sure that you have added the <code>path</code> package to the beginning of your <code>gulpfile.js</code></p>
<pre><code class="lang-JavaScript"><span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
</code></pre>
<blockquote>
<p>NOTE: In gulpfile, you should use the <code>lib</code> folder instead of <code>src</code>.</p>
</blockquote>
<h3 id="heading-registering-custom-aliases-in-spfx-fast-serve">Registering custom aliases in spfx-fast-serve</h3>
<p>After running the command <code>spfx-fast-serve</code> in your terminal (described above), the command added a new folder in the root of your project, called <code>fast-serve</code>, containing a file called <code>webpack.extend.js</code>. Again, we need to register the alias so <code>webpack</code> can resolve it.</p>
<p>There should be an (empty) variable <code>webpackConfig</code> in the file:</p>
<pre><code class="lang-JavaScript"><span class="hljs-keyword">const</span> webpackConfig = {
}
</code></pre>
<p>Now you have to extend this and register your alias:</p>
<pre><code class="lang-JavaScript"><span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
<span class="hljs-keyword">const</span> webpackConfig = {
  <span class="hljs-comment">/* CUSTOM ALIAS START */</span>
  <span class="hljs-attr">resolve</span>: {
      <span class="hljs-attr">alias</span>: {
        <span class="hljs-string">"@webparts"</span>: path.resolve(__dirname, <span class="hljs-string">".."</span>, <span class="hljs-string">"src/webparts"</span>),
        <span class="hljs-string">"@components"</span>: path.resolve(__dirname, <span class="hljs-string">".."</span>, <span class="hljs-string">"src/components"</span>),
        <span class="hljs-string">"@src"</span>: path.resolve(__dirname, <span class="hljs-string">".."</span>, <span class="hljs-string">"src"</span>),
      }
  },
  <span class="hljs-comment">/* CUSTOM ALIAS END */</span>
}
</code></pre>
<blockquote>
<p>NOTE: In <code>webpack.extend.js</code> you should use the <code>src</code> path again. And because the file is in a folder, you need to add a <code>'..'</code> after <code>__dirname</code> when calling the <code>path.resolve</code> method. And again, do not forget to import the path package!</p>
</blockquote>
<p>That's it, now you can use your defined alias instead of relative paths!</p>
<h2 id="heading-create-gulp-task-to-increase-the-package-version">Create Gulp task to increase the (package) version</h2>
<p>If you want to have a command to increment the SPFx solution version (<code>config/package-solution.json</code>) and the <code>package.json</code> version, then you should define a custom gulp task like I do. And even more, if you want to build the solution, increase the version and package the solution with one command, <a target="_blank" href="https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version">then you should read my post on how to do that</a>.</p>
<h2 id="heading-optional-steps">Optional steps</h2>
<p>I know the following steps aren't relevant to everyone or may not even appeal, but these are the ones I always do!</p>
<h3 id="heading-disable-css-class-name-warnings">Disable CSS class name warnings</h3>
<p>I often use CSS class names that do not conform to the Microsoft schema and are not camlCase. An example would be <code>spfxappdev-grid</code>.</p>
<p>When I define this, I get a warning</p>
<blockquote>
<p>Warning - [sass] The local CSS class 'spfxappdev-grid' is not camelCase and will not be type-safe.</p>
</blockquote>
<p>To remove this warning, you can suppress it in <code>gulpfile.js</code>. An SPFx project even suppresses such a warning for Microsoft's own CSS class by default. You can find it in the <code>gulpfile.js</code></p>
<pre><code class="lang-JavaScript">build.addSuppression(<span class="hljs-string">`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`</span>);
</code></pre>
<p>Of course, I could now add a new suppression by entering the following</p>
<pre><code class="lang-JavaScript">build.addSuppression(<span class="hljs-string">`Warning - [sass] The local CSS class 'spfxappdev-grid' is not camelCase and will not be type-safe.`</span>);
</code></pre>
<p>But I won't. Because I have more than just this one class. To suppress all these messages you can use RegEx:</p>
<pre><code class="lang-JavaScript">build.addSuppression(<span class="hljs-regexp">/Warning - \[sass\] The local CSS class/gi</span>);
</code></pre>
<p>That's it!</p>
<h3 id="heading-change-eslint-rules-typescript-rules">Change ESLint Rules / TypeScript rules</h3>
<p>Since SPFx v.1.15 was released, Microsoft switched from TSLint to ESLint. Actually a good decision, but some rules annoy me personally. Especially if you use <code>spfx-fast-serve</code> and you are still developing the modules. Sometimes you just want to test something fast and define variables that are not used (yet). Or you comment out a code and the method is now "empty". Then errors are output and often the <code>spfx-fast-serve</code> process is stopped =&gt; you have to start it again after fixing.</p>
<p>For this reason, I change the rules before starting development. To change them, open your <code>.eslintrc.js</code> and set your own values. For example, I change these rules to <code>0</code> (<code>0</code> = '0ff', <code>1</code> = 'warning', <code>2</code> = 'error'):</p>
<pre><code class="lang-JavaScript"><span class="hljs-string">'@typescript-eslint/no-explicit-any'</span>: <span class="hljs-number">0</span>,
<span class="hljs-string">'@typescript-eslint/no-floating-promises'</span>: <span class="hljs-number">0</span>,
<span class="hljs-string">'@typescript-eslint/no-unused-vars'</span>: [
          <span class="hljs-number">0</span>,
          {
            <span class="hljs-attr">vars</span>: <span class="hljs-string">'all'</span>,
            <span class="hljs-comment">// Unused function arguments often indicate a mistake in JavaScript code.  However in TypeScript code,</span>
            <span class="hljs-comment">// the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures</span>
            <span class="hljs-comment">// that are overriding a base class method or implementing an interface.</span>
            <span class="hljs-attr">args</span>: <span class="hljs-string">'none'</span>,
          },
]
</code></pre>
<p>And then I also change the <code>tsconfig.json</code>. I add these <code>compilerOptions</code>:</p>
<pre><code class="lang-json"><span class="hljs-string">"compilerOptions"</span>: {
    <span class="hljs-comment">// Other options...</span>
    <span class="hljs-attr">"noImplicitAny"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"noUnusedLocals"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"noUnusedParameters"</span>: <span class="hljs-literal">false</span>,
}
</code></pre>
<p>By the way, <a target="_blank" href="https://www.voitanos.io/blog/sharepoint-framework-v1.15-and-the-attack-of-eslint/">here is</a> a good article if you want to learn more about ESLint with SPFx.</p>
<h2 id="heading-i-dont-have-to-do-this-every-time-thanks-to-my-cli">I don't have to do this every time, thanks to my CLI</h2>
<p>Since I have done these above steps every time, for every project, I wanted to simplify it a bit. So I developed <a target="_blank" href="https://www.npmjs.com/package/@spfxappdev/cli">my own command line interface (CLI)</a> that does just that (<a target="_blank" href="https://spfx-app.dev/my-cli-for-spfx-development">Read more about my CLI</a>).</p>
<h2 id="heading-this-is-my-typical-folder-structure">This is my typical folder structure</h2>
<p>It has less to do with the topic of the article, but maybe someone is interested in what my folder structure looks like, which I (almost always) create. Of course not immediately, but only when I need it.</p>
<pre><code class="lang-bash">src
|- models
     |- interfaces
|- services
|- components
|- webparts
     |- components
|- ...
</code></pre>
<p>The <code>components</code> folder directly in the <code>src</code> folder is only for components I use in further SPFx modules (e.g. if I have more than one webpart or extension).</p>
<h2 id="heading-thats-it">That's it</h2>
<p>I hope you enjoyed the article and got some helpful tips. I would also be happy about feedback. Gladly also with your tips :)</p>
<p>Happy coding :)</p>
]]></content:encoded></item><item><title><![CDATA[New version of password vault webpart was released]]></title><description><![CDATA[Almost a year ago I released my "Simple password vault" webpart. Actually, it was only meant for a single username and password. If it should be more, you could have used the notes field. After I presented it once in the PnP community call, I also pr...]]></description><link>https://spfx-app.dev/new-version-of-password-vault-webpart-was-released</link><guid isPermaLink="true">https://spfx-app.dev/new-version-of-password-vault-webpart-was-released</guid><category><![CDATA[SharePoint]]></category><category><![CDATA[sPfx]]></category><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Thu, 13 Apr 2023 18:30:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681410573068/f7ca53e7-0ba4-4d6a-9035-4ac5055514cc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Almost a year ago I released my <a target="_blank" href="https://spfx-app.dev/a-simple-password-vault-webpart-for-microsoft-sharepointteams">"Simple password vault" webpart</a>. Actually, it was only meant for a single username and password. If it should be more, you could have used the notes field. After I presented it once in the <a target="_blank" href="https://www.youtube.com/watch?v=0snk0FWRMIY">PnP community call</a>, I also promised to release a new version with more features.</p>
<p>In the new version, it should be possible to store as many usernames, passwords and notes as you want. I finally got around to adding these features. I decided to make it dynamic, in a similar "look and feel" as in SharePoint Standard, when you want to place a new webpart. It appears this "+" symbol when you hover with the mouse.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681407851983/2ca285a3-f6dd-4f3e-936b-668455e911f3.png" alt="Add new Module &quot;+&quot; symbol" class="image--center mx-auto" /></p>
<p>And then you can add a new module. There are modules for username, password and also notes. Once modules are placed, you can sort or remove them.</p>
<p>The web part now looks like this (in edit mode):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681409419386/6c208a86-83c0-4a7f-a292-45b75f14f2e6.png" alt="New UI of edit mode" class="image--center mx-auto" /></p>
<p>I have also made a few design adjustments/optimizations. Also, for safety, the master password must be entered twice. And I have updated the solution to SPFx 1.16.1</p>
<p>In the "backend", the logic has changed a bit because of the modules. But I made sure that you can update from version 1.0.0 to the new version 1.1.0 without any problems. Without data loss but with the same functionality as in the new version. Even with the changes, the values are still encrypted.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681409859872/933df0fd-cb2e-4f86-9981-332fa82f00e9.png" alt class="image--center mx-auto" /></p>
<p>I hope you like my new webpart. You can download it <a target="_blank" href="https://github.com/SPFxAppDev/sp-passwordvault-webpart/releases/tag/v1.1.0">here</a></p>
]]></content:encoded></item><item><title><![CDATA[Using pnpm in SPFx projects]]></title><description><![CDATA[Typically, an SPFx Project is scaffolded via yo and using the yo @microsoft/sharepoint command. By default, all required node packages are installed using npm as the package manager. But you can also use other package managers like yarn or pnpm. I do...]]></description><link>https://spfx-app.dev/using-pnpm-in-spfx-projects</link><guid isPermaLink="true">https://spfx-app.dev/using-pnpm-in-spfx-projects</guid><category><![CDATA[sPfx]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[SharePoint Online]]></category><category><![CDATA[#sharepoint-framework]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Thu, 09 Feb 2023 19:06:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1675968665027/7bcc3856-1685-46e3-bf9d-9368bf179f86.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Typically, an SPFx Project is scaffolded via <code>yo</code> and using the <code>yo @microsoft/sharepoint</code> command. By default, all required node packages are installed using <a target="_blank" href="https://www.npmjs.com/">npm</a> as the package manager. But you can also use other package managers like <a target="_blank" href="https://yarnpkg.com/">yarn</a> or <a target="_blank" href="https://pnpm.io/">pnpm</a>. I do not want to explain the difference or how <code>pnpm</code> works here, but I can definitively say that <code>pnpm</code> is a better choice than <code>npm</code> when it comes to creating new projects.</p>
<p>I have always used the "normal" way via <code>npm</code> and never dealt with <code>yarn</code> or <code>pnpm</code> before. When I read up on the different package managers, it was immediately clear I should go for <code>pnpm</code>. It is (almost ever) faster and the file size and also the number of files are smaller.</p>
<p>After some research on the Internet, I found two very good articles by <a target="_blank" href="https://www.m365-dev.com/2018/10/31/using-pnpm-with-spfx/">Joel Rodrigues</a> and <a target="_blank" href="https://www.voitanos.io/blog/npm-yarn-pnpm-which-package-manager-should-you-use-for-sharepoint-framework-projects/">Andrew Connell</a>, both Microsoft MVPs. Unfortunately, I ran into a few problems when I tried it out. This may be due to SPFx (I use SPFx 1.16.1) or because both articles are from 2018. That is why I decided to write this article. It also contains a tip that each of you can use. Let me surprise you.</p>
<p>Because I reinstalled my PC a few days ago, I had to set everything up again. For this reason, I reinstalled <code>yo</code>, <code>gulp</code> and <code>@microsoft/generator-sharepoint</code> but directly with <code>pnpm</code>. So I took the command from the <a target="_blank" href="https://learn.microsoft.com/de-de/sharepoint/dev/spfx/set-up-your-development-environment#install-development-toolchain-prerequisites">Microsoft article</a> and replaced <code>npm</code> with <code>pnpm</code>.</p>
<pre><code class="lang-bash">pnpm install gulp-cli yo @microsoft/generator-sharepoint --global --shamefully-hoist
</code></pre>
<p>However, this is not necessary if you have already installed everything via <code>npm</code>, because <code>gulp</code> and <code>yo</code> can be used globally.</p>
<h2 id="heading-install-pnpm">Install <code>pnpm</code></h2>
<p>Of course, before you can use <code>pnpm</code>, you must first install it. I used the <code>PowerShell</code> command is described on the installation page.</p>
<pre><code class="lang-bash"> iwr https://get.pnpm.io/install.ps1 -useb | iex
</code></pre>
<p>But if you prefer <code>npm</code> or another method, just have a look at the <a target="_blank" href="https://pnpm.io/installation">official installation guide</a>.</p>
<blockquote>
<p><strong>Note</strong>: Please check the <a target="_blank" href="https://pnpm.io/installation#compatibility">compatibility</a> of <code>pnpm</code> with your installed <code>node</code> version</p>
</blockquote>
<h2 id="heading-create-a-new-spfx-project-with-pnpm">Create a new SPFx Project with <code>pnpm</code></h2>
<p>If you want to create a new project you can use the well-known <code>yo @microsoft/sharepoint</code> command but you have to add the argument <code>--package-manager pnpm</code>.</p>
<pre><code class="lang-bash">yo @microsoft/sharepoint --package-manager pnpm
</code></pre>
<p>Now the standard build process is done and after that all needed <code>npm</code> packages are installed but with <code>pnpm</code>. The installation of the packages was fast, wasn't it?</p>
<p>I was really impressed by how fast and easy it was. So I wanted to test the standard webpart. But <code>gulp serve</code> threw errors. The package <code>@microsoft/sp-component-base</code> could not be found. Also in the SCSS file the reference to Fluent UI (<code>@import '~@fluentui/react/dist/sass/References.scss';</code>) could not be found.</p>
<p>After some research I found out that I should install all packages with the additional argument <a target="_blank" href="https://pnpm.io/npmrc#shamefully-hoist">--shamefully-hoist</a>.</p>
<p>To get a "clean" installation, I deleted the <code>node_modules</code> folder (<a target="_blank" href="https://spfx-app.dev/delete-the-nodemodules-folder-50-faster-on-windows">here is an article of mine describing how to do it faster</a>). Additionally, I deleted the file <code>pnpm-lock.yaml</code> (the equivalent to <code>package-lock.json</code> from <code>npm</code>). Then I executed the command</p>
<pre><code class="lang-bash">pnpm install --shamefully-hoist
</code></pre>
<p>Now also much more files came within the <code>node_modules</code> folder and also a <code>gulp serve</code> could be executed without errors.</p>
<p>You will probably wonder if you have to delete the folder or run a <code>pnpm install --shamefully-hoist</code> every time you want to create a new project. This would be an option, but not a very good one. A better one is to set this option in the <code>.npmrc</code> file. There are two possibilities.</p>
<h3 id="heading-global-npmrc">Global <code>.npmrc</code></h3>
<p>To set the Global option, i.e. a rule that should apply to all projects, you can enter this command:</p>
<pre><code class="lang-bash">pnpm config <span class="hljs-built_in">set</span> shamefully-hoist <span class="hljs-literal">true</span>
</code></pre>
<p><strong>But</strong>: the <code>.npmrc</code> file is used by both <code>pnpm</code> and <code>npm</code>. So if you want to have custom settings ONLY for <code>pnpm</code>, then this should not be set globally (admittedly this setting does not exist for <code>npm</code>, so it could be set globally)</p>
<h3 id="heading-locale-npmrc">Locale <code>.npmrc</code></h3>
<p>Before you run <code>yo @microsoft/sharepoint --package-manager pnpm</code>, run this command in the project directory:</p>
<pre><code class="lang-bash">pnpm config <span class="hljs-built_in">set</span> shamefully-hoist <span class="hljs-literal">true</span> --location project
</code></pre>
<p>Then it will apply only to the current project/folder. The disadvantage here, however, is that this step must not be forgotten. It must be important that it is executed before the <code>yo @microsoft/sharepoint...</code> command.</p>
<h2 id="heading-optional-only-allow-pnpm-usage">Optional: Only allow <code>pnpm</code> usage</h2>
<p>When you use <code>pnpm</code> on a project, you do not want others to accidentally run <code>npm install</code> or <code>yarn</code>. To prevent developers from using other package managers, you can modify the <code>package.json</code> as follows:</p>
<pre><code class="lang-json">    <span class="hljs-string">"scripts"</span>: {
        <span class="hljs-attr">"preinstall"</span>: <span class="hljs-string">"npx only-allow pnpm"</span>
        <span class="hljs-comment">//other package.json scripts</span>
    }
<span class="hljs-comment">//other package.json settings</span>
</code></pre>
<h2 id="heading-npm-and-pnpm-compared"><code>npm</code> and <code>pnpm</code> compared</h2>
<p>I have created two different projects, both are webpart projects with <code>React</code> and the same name. Only one with <code>npm</code> and the other with <code>pnpm</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675958099247/eb4c6b86-ac8c-483e-bf66-138fe6f9566c.png" alt="npm and pnpm compared" class="image--center mx-auto" /></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td>npm</td><td>pnpm</td></tr>
</thead>
<tbody>
<tr>
<td>Size</td><td>660 MB</td><td>472 MB (~30% smaller)</td></tr>
<tr>
<td>Files</td><td>~75k</td><td>~57k (~25% less)</td></tr>
<tr>
<td>Folders</td><td>~9k</td><td>~11k (~20% more)</td></tr>
</tbody>
</table>
</div><h2 id="heading-summary">Summary</h2>
<ol>
<li><p>If you have chosen a</p>
<ol>
<li><p><a class="post-section-overview" href="#heading-locale-npmrc">locale "strategy"</a> of the <code>.npmrc</code> file, then before creating a project you have to execute this command <code>pnpm config set shamefully-hoist true --location project</code></p>
</li>
<li><p><a class="post-section-overview" href="#heading-global-npmrc">global "strategy"</a> of the <code>.npmrc</code> file, then you don't need to do anything (as long as you run it once)</p>
</li>
</ol>
</li>
<li><p>Create a new project as follows: <code>yo @microsoft/sharepoint --package-manager pnpm</code></p>
</li>
<li><p><strong>Optional</strong>: Change the <code>package.json</code> file to prevent other developers from using other package managers</p>
</li>
</ol>
<h2 id="heading-bonus">Bonus</h2>
<p>As promised, here is a bonus. Now I will show you how to make it even easier and more automated. The commands are very long and if you have chosen a <a class="post-section-overview" href="#heading-locale-npmrc">locale "strategy"</a> of the <code>.npmrc</code> file, then you must always remember to run the other commands first. Maybe you already know the way to define aliases via the registry (<code>regedit</code>) entries or via a <code>cmd</code> file and include (this <code>cmd</code> file) it in the PATH environment variable. But here I want to show something different. And that is about <code>PowerShell</code> aliases and profiles.</p>
<p>Open a new <code>PowerShell</code> window <strong>as administrator</strong> and run</p>
<pre><code class="lang-bash">notepad <span class="hljs-variable">$profile</span>.AllUsersAllHosts
</code></pre>
<p>If a message appears that no profile exists yet, create one. Otherwise, use your existing profile and edit the script file that opens. Now paste this code:</p>
<pre><code class="lang-bash"><span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">spfxWithPnpm</span></span>() {
    pnpm config <span class="hljs-built_in">set</span> auto-install-peers <span class="hljs-literal">true</span> --location project
    pnpm config <span class="hljs-built_in">set</span> shamefully-hoist <span class="hljs-literal">true</span> --location project
    yo @microsoft/sharepoint --package-manager pnpm
}

<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">spfxWithNpm</span></span>() {
    yo @microsoft/sharepoint
}

set-alias -name spfxNpm -value spfxWithNpm -Scope Global
set-alias -name spfx -value spfxWithPnpm -Scope Global
set-alias -name pn -value pnpm -Scope Global
</code></pre>
<p>There are two <code>PowerShell</code> functions. One for the case that a project should still be created with <code>npm</code>. And the other one with <code>pnpm</code>. You could now simply run the <code>spfxWithPnpm</code> command from any <code>PowerShell</code> terminal and it would apply the (local "strategy")(#heading-local-npmrc) of the <code>.npmrc</code> file and create the project with <code>pnpm</code>. Or you could write <code>spfxWithNpm</code> to build it with <code>npm</code>. But to make it even shorter, <code>PowerShell</code> aliases are defined for the functions (yes, you can also just rename the functions, then you don't need the aliases anymore. Or you can handle the complete logic in one function and work with parameters). Finally, an alias is created for <code>pnpm</code> itself, so that you can write <code>pn</code> instead of <code>pnpm</code> (you can of course use whatever you like).</p>
<p>By the way: I have included <code>auto-install-peers</code> in the <code>spfxWithPnpm</code> function. This is an example if you want to make this setting only for <code>pnpm</code> but not for <code>npm</code>. As described before, both package managers use this file. But unlike <code>shamefully-hoist</code>, <code>auto-install-peers</code> is also available for <code>npm</code>. If you choose a global strategy for <code>auto-install-peers</code>, then it affects both <code>npm</code> and <code>pnpm</code>.</p>
]]></content:encoded></item><item><title><![CDATA[Copy & paste like a pro with the Windows clipboard history]]></title><description><![CDATA[Have you ever had a situation in which you had to jump back and forth between different copied values? First, you had to copy value A and paste it to location A, then you had to copy value B and paste it to location B. On top of that the worst case: ...]]></description><link>https://spfx-app.dev/copy-paste-like-a-pro-with-the-windows-clipboard-history</link><guid isPermaLink="true">https://spfx-app.dev/copy-paste-like-a-pro-with-the-windows-clipboard-history</guid><category><![CDATA[windows11]]></category><category><![CDATA[Windows]]></category><category><![CDATA[windows10]]></category><category><![CDATA[Windows 10]]></category><category><![CDATA[windows 11]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Wed, 09 Nov 2022 12:30:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1667992695721/3jzy1USiN.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever had a situation in which you had to jump back and forth between different copied values? First, you had to copy value A and paste it to location A, then you had to copy value B and paste it to location B. On top of that the worst case: you have to jump between different tabs/documents/applications to copy the values. You then also have to repeat this more often, because it should be pasted more often. Another situation could also be that something was copied from a website. In the meantime, you have already copied something else. But now you need again the value of the already closed website. So you search again for this website. So a history of the clipboard would be great, wouldn't it?</p>
<p>The great thing is, such a thing exists. And even better, it can be used by everyone by default starting from Windows 10. It's called Clipboard History. You have to enable this feature once. To do that, you just press the key combination <code>Win (Logo) + V</code>. You will be prompted to activate it:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667992153604/815a_kVAl.png" alt="Turn on clipboard history" class="image--center mx-auto" /></p>
<p>If you do not see this screen, then you have already activated it!</p>
<p><strong>By the way:</strong> You can check/change the setting or view advanced settings at any time via <code>Settings</code> =&gt; <code>System</code> =&gt; <code>Clipboard</code> =&gt; <code>Clipboard history</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667992335095/-cnzXd6un.png" alt="Clipboard settings in Windows 10" class="image--center mx-auto" /></p>
<p>Now you can copy your item as usual with <code>Ctrl + C</code> and then display the history with <code>Win (Logo) + V</code> and paste it from there. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667992415434/gXvQj3fDz.png" alt="The clipboard history" class="image--center mx-auto" /></p>
<p>The history stores up to 25 last copied items and is automatically cleared after a restart of the computer. You can also pin certain items there, which will then still be listed after the reboot. Just click on the three dots "..." and select "Pin".</p>
<p>That's it. I use it a lot and it really helps me a lot. I hope for you too.</p>
<p>Happy pasting ;)</p>
]]></content:encoded></item><item><title><![CDATA[SPFx Solutions throws error "minified React error #321"]]></title><description><![CDATA[This time a short post. Some of my customers sent me the same error today. A solution that worked for months suddenly no longer works. The error message:
ERROR:
Minified React error #321; visit https://reactjs.org/docs/error-decoder.html?invariant=32...]]></description><link>https://spfx-app.dev/spfx-solutions-throws-error-minified-react-error-321</link><guid isPermaLink="true">https://spfx-app.dev/spfx-solutions-throws-error-minified-react-error-321</guid><category><![CDATA[fluent ui]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[React]]></category><category><![CDATA[SharePoint]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Tue, 18 Oct 2022 10:13:31 GMT</pubDate><content:encoded><![CDATA[<p>This time a short post. Some of my customers sent me the same error today. A solution that worked for months suddenly no longer works. The error message:</p>
<pre><code class="lang-javascript">ERROR:
Minified React error #<span class="hljs-number">321</span>; visit https:<span class="hljs-comment">//reactjs.org/docs/error-decoder.html?invariant=321 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.</span>

CALL STACK:
<span class="hljs-built_in">Error</span>: Minified React error #<span class="hljs-number">321</span>; visit https:<span class="hljs-comment">//reactjs.org/docs/error-decoder.html?invariant=321 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.</span>
    at <span class="hljs-built_in">Object</span>.ao (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/sp-pages-assembly_en-us_ea3e1b4c1c4e9e32890807c4b77664b2.js:69:64371)</span>
    at t.useRef (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/sp-pages-assembly_en-us_ea3e1b4c1c4e9e32890807c4b77664b2.js:3:6809)</span>
    at https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/office-ui-fabric-react-bundle_none_441bf6f5d0719b2d3d2a.js:1:40124</span>
    at Yr (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/react-dom-16-bundle_none_8eaea77e9a62cc734837.js:2:64000)</span>
    at To (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/react-dom-16-bundle_none_8eaea77e9a62cc734837.js:2:71934)</span>
    at ys (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/react-dom-16-bundle_none_8eaea77e9a62cc734837.js:2:112131)</span>
    at pc (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/react-dom-16-bundle_none_8eaea77e9a62cc734837.js:2:102665)</span>
    at uc (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/react-dom-16-bundle_none_8eaea77e9a62cc734837.js:2:102590)</span>
    at ac (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/react-dom-16-bundle_none_8eaea77e9a62cc734837.js:2:99620)</span>
    at Zs (https:<span class="hljs-comment">//res-1.cdn.office.net/files/sp-client/react-dom-16-bundle_none_8eaea77e9a62cc734837.js:2:96388)</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1666087533161/FpMpyrBlN.png" alt="minified React error #321" /></p>
<p>Apparently something has changed at Microsoft. Because nothing was changed on the customer side. </p>
<p>So I tried around a bit and then came across the reason. All the Fluent UI components produce the errors.</p>
<h1 id="heading-solution-how-to-fix">Solution / How to fix</h1>
<p>For some reason an error occurs when importing from <code>@microsoft/office-ui-fabric-react-bundle</code> instead of <code>office-ui-fabric-react</code>.</p>
<p>So I replaced the following "import":</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Icon, IconButton, PrimaryButton } <span class="hljs-keyword">from</span> <span class="hljs-string">'@microsoft/office-ui-fabric-react-bundle'</span>;
</code></pre>
<p>like this</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Icon, IconButton, PrimaryButton } <span class="hljs-keyword">from</span> <span class="hljs-string">'office-ui-fabric-react'</span>;
</code></pre>
<p>That's it. I hope this helps you</p>
<h1 id="heading-update">Update</h1>
<p>After publishing my post, I was informed of the reason by a community member. </p>
<pre><code>For Clarity, Microsoft updated their internal versions <span class="hljs-keyword">of</span> React to React <span class="hljs-number">17.</span> 
They are working on a fix to allow usage <span class="hljs-keyword">of</span> <span class="hljs-built_in">this</span> 
office-ui-fabric-react-bundle without issue.

However, <span class="hljs-built_in">this</span> is the correct fix <span class="hljs-keyword">if</span> you don<span class="hljs-string">'t have 
time to wait for Microsoft to remedy</span>
</code></pre><p><a target="_blank" href="https://github.com/SharePoint/sp-dev-docs/issues/8487">Additionally, you can find more information in this Github issue</a>: </p>
]]></content:encoded></item><item><title><![CDATA[Browser extension for (dynamic) quick links for SharePoint Online]]></title><description><![CDATA[This time I wanted to try something new and develop a browser extension, my first ever. The idea came to me spontaneously: An extension that offers various quick links to SharePoint, such as site contents, site settings, site permissions, etc. This s...]]></description><link>https://spfx-app.dev/browser-extension-for-dynamic-quick-links-for-sharepoint-online</link><guid isPermaLink="true">https://spfx-app.dev/browser-extension-for-dynamic-quick-links-for-sharepoint-online</guid><category><![CDATA[chrome extension]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[SharePoint Online]]></category><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Wed, 28 Sep 2022 07:21:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1664182177056/0pvDpdsF0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This time I wanted to try something new and develop a <a target="_blank" href="https://chrome.google.com/webstore/detail/sharepoint-quicklinks/ehmphjkdhndbdpfhhckemmhapfahlbhe">browser extension</a>, my first ever. The idea came to me spontaneously: An extension that offers various quick links to SharePoint, such as site contents, site settings, site permissions, etc. This saves a lot of "clicks". Who doesn't know it? A site is loaded, then it takes a while until the "⚙ settings/gear" icon is displayed. Then you (usually) have to make several clicks to get to "Site settings", for example. On a Modern Page, this is typically done via ⚙settings/gear icon <strong>=&gt;</strong> Site information <strong>=&gt;</strong> View all site settings. Of course, you can also "quickly" enter the URL in the browser. You probably know the URL by heart, don't mistype it, and are "faster" than the UI and the mouse clicks.</p>
<p>My idea is a kind of "favorites" list for SharePoint, so to speak. But why not use the favorites function of the browser? Quite simply, a URL that has been marked as a favorite is a static URL. So you can create a URL to the <code>site contents</code> of <code>Site A</code> and one for <code>Site B</code>. But not to <code>Site contents</code> of the current site collection you are on. And that was the idea. An extension that reads the current URL of the tab/window and makes the URL "dynamic". In the extension, the URL <code>{weburl}/_layouts/15/viewlsts.aspx</code> is replaced by the current web URL. If you are on <code>https://tenantname.sharepoint.com/sites/siteA/SitePages/MyPage.aspx</code>, the placeholder <code>{weburl}</code> is replaced by <code>https://tenantname.sharepoint.com/sites/siteA</code>. In this way <code>{weburl}/_layouts/15/viewlsts.aspx</code> becomes <code>https://tenantname.sharepoint.com/sites/siteA/_layouts/15/viewlsts.aspx</code>. The extension should have the following features:</p>
<ul>
<li><p>First: it should be simple and fast to implement (at least for now, the next versions can be extended). I don't have any experience with extensions yet, so it should be rather simple.</p>
</li>
<li><p>Reading the current Tab/Windows URL</p>
</li>
<li><p>If it is not a <code>.sharepoint.com</code> domain, the extension should not be usable and a hint text should be displayed</p>
</li>
<li><p>No SharePoint API calls should be made to read out the URLs or to get the current user. The URL should only be "built up" using the browser URL of the current tab. This is helpful for performance reasons, but also for time and data protection reasons. An extension that makes an API call in the tab context, for example, is checked more strictly and longer by the Google team.</p>
</li>
<li><p>Due to the aforementioned requirement, the links are always displayed and it is not checked whether the user has the authorization to call up this link.</p>
</li>
<li><p>The extension should not store SharePoint data or forward it to third parties.</p>
</li>
<li><p>It comes with some "quick links" that I find helpful. These cannot be changed, but they can be de- and activated. And the label can be changed.</p>
</li>
<li><p>You can store your URLs, which are then also displayed and dynamically "replaced". </p>
</li>
<li><p>There should be the placeholders <code>{webappurl}</code>, <code>{weburl}</code>, and <code>{pageurl}</code>.</p>
</li>
<li><p>Only links that have been "enabled" by the user shall be displayed. </p>
</li>
<li><p>The placeholder <code>{pageurl}</code> is replaced by the current (SharePoint) page URL. If you are not on a page, the links are not displayed.</p>
</li>
<li><p>For me as a SharePoint developer and all other SharePoint developers, some URLs are of course also very useful for SPFx development. Therefore, in addition to the "normal" <code>loadSPFx=true</code> URL parameters (with which you can test the SPFx WebParts on the current page), there is also the option of loading an SPFx Application Customizer. For this purpose, the App Id is requested (prompt dialog) as soon as you click on the URL.</p>
</li>
<li><p>It should be available for Chrome and all browsers that use the Chrome Extension Web Store.</p>
</li>
</ul>
<h2 id="heading-result">Result</h2>
<p>This is how <a target="_blank" href="https://chrome.google.com/webstore/detail/sharepoint-quicklinks/ehmphjkdhndbdpfhhckemmhapfahlbhe">my extension</a> looks like:</p>
<h4 id="heading-popup">PopUp</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1664349094421/JyHe_FjHi.png" alt="PopUp" /></p>
<h4 id="heading-configuration">Configuration</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1664349039976/QqvgoR6Df.png" alt="Configure custom URLs" /></p>
<h4 id="heading-spfx-app-debug">SPFx App Debug</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1664349618586/VR_IDJd5I.png" alt="Debug a SPFx App" /></p>
<h2 id="heading-download">Download</h2>
<p><a target="_blank" href="https://chrome.google.com/webstore/detail/sharepoint-quicklinks/ehmphjkdhndbdpfhhckemmhapfahlbhe">Feel free to download my extension. I am happy about your feedback</a>. You can install the extension on all Chromium-based browsers (Edge Chromuim, Google Chrome, Brave etc.). You can download it directly via the <a target="_blank" href="https://chrome.google.com/webstore/detail/sharepoint-quicklinks/ehmphjkdhndbdpfhhckemmhapfahlbhe">link</a> or by searching for "SharePoint Quicklinks" in the Extension Store.</p>
<p>You can also find the source code for this extension in my <a target="_blank" href="https://github.com/SPFxAppDev/sp-quicklinks-browser-extension">GitHub repository</a></p>
]]></content:encoded></item><item><title><![CDATA[SPFx Azure DevOps Pipeline: Increment version, push to repository and publish package]]></title><description><![CDATA[Last week I published a blog post describing how to increment the version of the SPFx solution using a gulp task. After that, people asked in the comments how to map the whole thing in an Azure DevOps pipeline so that the pipeline automatically incre...]]></description><link>https://spfx-app.dev/spfx-azure-devops-pipeline-increment-version-push-to-repository-and-publish-package</link><guid isPermaLink="true">https://spfx-app.dev/spfx-azure-devops-pipeline-increment-version-push-to-repository-and-publish-package</guid><category><![CDATA[Microsoft]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[Azure Pipelines]]></category><category><![CDATA[YAML]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Mon, 20 Jun 2022 18:57:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1655751383719/HGxynoqrP.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last week I published a <a target="_blank" href="https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version">blog post</a> describing how to increment the version of the SPFx solution using a <code>gulp task</code>. After that, people asked in the comments how to map the whole thing in an Azure DevOps pipeline so that the pipeline automatically increments the version and writes this updated version back to the repository. A very good question, I think, which I actually wanted to answer later (for myself). So far I only had the "normal" pipeline that publishes the package. Now the challenge came a bit earlier than I thought.</p>
<h2 id="heading-the-idea-is-quite-simple">The idea is quite simple</h2>
<p>When pushing to main/master branch, the Azure DevOps pipeline should automatically check out the branch, increment the version, build the package, check the changes back into the repository and (optionally) create a Git tag with the version number to make it clearer. You can turn off tagging or version incrementing in the pipeline. In addition, you can use the commit message to determine which part of the version (major, minor or patch) should be updated.</p>
<h2 id="heading-here-we-go">Here we go</h2>
<p>Before you start with the pipeline and the <code>.yaml</code> file, you have to configure something. In the project settings under <code>Repository</code> =&gt; <code>{NameOfRepository}</code> =&gt; <code>Security</code> =&gt; <code>USER: {NameOfRepository} Build Service</code>. This user must be given the permissions <code>Read</code>, <code>Contribute</code>, <code>Create tag</code>, <code>Create branch</code> and <code>Contribute to pull requests</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655748596790/6_W-LOIiu.png" alt="Userpermission settings in Azure DevOps" /></p>
<h3 id="heading-the-yaml-file">The <code>yaml</code> file</h3>
<blockquote>
<p>NOTE: This post and the yaml file were updated on January 25, 2024.</p>
</blockquote>
<p>I won't show you how to create a pipeline but will go into the individual steps in the <code>yaml</code> file. If that doesn't interest you, you can jump straight to the <a class="post-section-overview" href="#heading-the-complete-file">final file</a> and use it.</p>
<h4 id="heading-step-1-checkout">Step 1: Checkout</h4>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span> <span class="hljs-string">self</span>
  <span class="hljs-attr">clean:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">persistCredentials:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>This step determines how the pipeline should check out the source code.</p>
<h4 id="heading-step-2-check-if-gitconfig-exists">Step 2: Check if gitconfig exists</h4>
<p>Sometimes, the pipeline might fail at step 3 if the .gitconfig file already exists. So, in this step, we check if it's there. I've only made it show whether the file exists or not. You can change the script to delete the file or create a variable to use as a "condition" in step 3.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">powershell:</span> <span class="hljs-string">|
   $exists = Test-Path -Path $HOME/.gitconfig
   WRITE-HOST ".gitconfig exist:" $exists
</span>  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Check</span> <span class="hljs-string">whether</span> <span class="hljs-string">gitconfig</span> <span class="hljs-string">exists</span>
</code></pre>
<h4 id="heading-step-3-set-git-user">Step 3: Set Git user</h4>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
   git config --global user.email devops@spfx-app.dev &amp; git config --global user.name "spfx-app.dev".
</span>  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
</code></pre>
<p>This sets the user to be displayed at (code) checkin/push. The user does not have to actually exist. It will only be displayed in Azure DevOps as specified.</p>
<h4 id="heading-step-4-powershell-to-set-the-output-variables">Step 4: PowerShell to set the output variables</h4>
<p>As mentioned at the beginning, the version should only be incremented if it is desired (pipeline variable <code>CI_BUMP_VERSION</code> is set to <code>TRUE</code>). In addition, the commit message <code>Build.SourceVersionMessage</code> can be used to determine which part of the version (<code>major</code>, <code>minor</code>, or <code>patch</code>) should be incremented. By default, the patch version is always incremented.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">powershell:</span> <span class="hljs-string">|
   $commitMsg = "$(Build.SourceVersionMessage)"
   Write-Host $commitMsg
   Write-Host "Version bump is ENABLED"
</span>
   <span class="hljs-string">$bumpVersionArgs</span> <span class="hljs-string">=</span> <span class="hljs-string">"--no-patch"</span>
   <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_MAJOR_VERSION;]$false"</span>
   <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_MINOR_VERSION;]$false"</span>
   <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_PATCH_VERSION;]$false"</span>
   <span class="hljs-string">if($commitMsg</span> <span class="hljs-string">-match</span> <span class="hljs-string">"(major):.*"</span><span class="hljs-string">)</span> {
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"Bump Major Version"</span>
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_MAJOR_VERSION;]$true"</span>
      <span class="hljs-string">$bumpVersionArgs</span> <span class="hljs-string">=</span> <span class="hljs-string">"--major"</span>
    }
    <span class="hljs-string">elseif($commitMsg</span> <span class="hljs-string">-match</span> <span class="hljs-string">"(minor):.*"</span><span class="hljs-string">)</span> {
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"Bump Minor Version"</span>
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_MINOR_VERSION;]$true"</span>
      <span class="hljs-string">$bumpVersionArgs</span> <span class="hljs-string">=</span> <span class="hljs-string">"--minor"</span>
    }
    <span class="hljs-string">else</span> {
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"Bump Patch Version"</span>
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_PATCH_VERSION;]$true"</span>
      <span class="hljs-string">$bumpVersionArgs</span> <span class="hljs-string">=</span> <span class="hljs-string">"--patch"</span>
    }

    <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_VERSION_ARGS;]$bumpVersionArgs"</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Get</span> <span class="hljs-string">the</span> <span class="hljs-string">commit</span> <span class="hljs-string">message</span>
  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">)</span>
</code></pre>
<p>If the commit message contains a <code>minor:</code> the minor version is incremented. If the commit message contains a <code>major:</code> the major version is incremented. It is not necessary to specify <code>patch:</code> as it will increment the patch version by default, but this message would increment the patch version. Depending on which version part is updated, the argument for the later <code>gulp bump</code> command is stored in the variable <code>BUMP_VERSION_ARGS</code> to be passed in step 10.</p>
<p><code>Note</code>: for a pull request, the "Title" column of the form in the UI corresponds to the variable <code>Build.SourceVersionMessage</code>.</p>
<h4 id="heading-step-5-use-nodejs">Step 5: Use <code>Node.js</code></h4>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">NodeTool@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Use Node 16.x'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">versionSpec:</span> <span class="hljs-number">16.</span><span class="hljs-string">x</span>
    <span class="hljs-attr">checkLatest:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>I think it is clear what this step does. Node version 16.x will be installed.</p>
<p><code>Note</code>: Of course you have to adapt the version of Node to your SPFx version. A list of supported Node versions can be found <a target="_blank" href="https://docs.microsoft.com/en-us/sharepoint/dev/spfx/compatibility">here</a></p>
<h4 id="heading-step-6-install-pnpm-optional">Step 6: install pnpm (OPTIONAL)</h4>
<p>This step is optional, but if you use <code>pnpm</code> instead of <code>npm</code>, you'll need to install pnpm first.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">Npm@1</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'npm install -g pnpm'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">workingDir:</span> <span class="hljs-string">' $(Build.Repository.LocalPath)'</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">'custom'</span>
    <span class="hljs-attr">customCommand:</span> <span class="hljs-string">'install -g pnpm'</span>
    <span class="hljs-attr">verbose:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>If you use <code>pnpm</code>, please remember to enable this step.</p>
<h4 id="heading-step-7-npm-install">Step 7: <code>npm install</code></h4>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">CmdLine@2</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'npm install'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">script:</span> <span class="hljs-string">'npm i'</span>
    <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">' $(Build.Repository.LocalPath)'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>All <code>npm</code> packages will now be installed.</p>
<h4 id="heading-step-8-gulp-clean">Step 8: <code>gulp clean</code></h4>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp clean'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">clean</span>
</code></pre>
<p>The <code>gulp</code> task <code>gulp clean</code> is executed.</p>
<h4 id="heading-step-9-gulp-build-optional">Step 9: gulp build (OPTIONAL)</h4>
<p>This step is not necessary, but if you'd like, you can enable it and run the <code>gulp build --ship</code> command.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp build --ship'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">build</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">'--ship'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">false</span>
</code></pre>
<h4 id="heading-step-10-gulp-bump-version">Step 10 <code>gulp bump-version</code></h4>
<p>Now the task I described in the <a target="_blank" href="https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version">last blog post</a> is used to increment the version. Again, only if the pipeline variable <code>CI_BUMP_VERSION</code> has been set to <code>TRUE</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp bump-version'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">'bump-version'</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">$(BUMP_VERSION_ARGS)</span>
  <span class="hljs-attr">continueOnError:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">)</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>The stored value from the variable <code>BUMP_VERSION_ARGS</code> from <a class="post-section-overview" href="#heading-step-3-powershell-to-set-the-output-variables">step 3</a> is passed as an argument (<code>--major</code>, <code>--minor</code> or <code>--patch</code>).</p>
<h4 id="heading-step-11-gulp-bundle-ship">Step 11: <code>gulp bundle --ship</code></h4>
<p>The <code>gulp</code> task <code>bump-version</code> is normally executed automatically before the <code>gulp bundle --ship</code> task (see <a target="_blank" href="https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version">my blog post</a>). But as you can tell the pipeline whether the version should be incremented or not, I had to split this task and pass the additional argument <code>--no-ship</code> to the <code>gulp bundle --ship</code> so that the <code>bump-version</code> task is not executed again.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp bundle --ship --no-patch'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">bundle</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">'--ship --no-patch'</span>
  <span class="hljs-attr">continueOnError:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<h4 id="heading-step-12-gulp-package-solution-ship">Step 12: <code>gulp package-solution --ship</code></h4>
<p>I think this step is self-explanatory. The solution is packed.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp package-solution --ship'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">'package-solution'</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">'--ship'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<h4 id="heading-step-13-read-updated-version-from-packagejson">Step 13: Read updated version from <code>package.json</code></h4>
<p>If the version was updated, it must of course be read again (for the later Git tag and also the commit message). I did this with a PowerShell script.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">powershell:</span> <span class="hljs-string">|
   $newVersion = (Get-content ./package.json| out-string | ConvertFrom-Json).version
   Write-Host "##vso[task.setvariable variable=NEW_VERSION;]$newVersion"
   Write-Host $newVersion
</span>  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Get</span> <span class="hljs-string">Version</span> <span class="hljs-string">from</span> <span class="hljs-string">package.json</span> <span class="hljs-string">and</span> <span class="hljs-string">set</span> <span class="hljs-string">in</span> <span class="hljs-string">variable</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">)</span>
</code></pre>
<p>The <code>package.json</code> file is read and the version number is stored in the variable <code>NEW_VERSION</code>.</p>
<h4 id="heading-step-14-push-back-to-repository">Step 14: push back to Repository</h4>
<p>Now the version is restored to the repository if the version has been updated.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
   echo $(NEW_VERSION)
   git add .
   git commit -m "[skip ci] Version updated to $(NEW_VERSION)"
   git push origin HEAD:$(Build.SourceBranch)
</span>  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Push</span> <span class="hljs-string">changes</span> <span class="hljs-string">to</span> <span class="hljs-string">source</span> <span class="hljs-string">branch</span>
  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">)</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>All changes are checked in with the message <code>[skip ci] version updated to $(NEW_VERSION)</code>. The <code>[skip ci]</code> is very important. In case you have configured an automatic execution of the pipeline on the (main/master) branch, it would execute the pipeline again (continuous loop). This can be prevented with this text in the commit message (see: https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&amp;tabs=yaml#skipping-ci-for-individual-commits).</p>
<h4 id="heading-step-15-create-git-tag">Step 15: Create Git tag</h4>
<p>I thought it was helpful that a Git tag is created at the same time as the version update and after the check in. The name of the tag is then the version number. This is only done if the variables <code>CI_BUMP_VERSION</code> and <code>CI_CREATE_GIT_TAG</code> are both set to <code>TRUE</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
   git tag $(NEW_VERSION) HEAD
   git push origin --tags
</span>  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Create</span> <span class="hljs-string">Tag</span> <span class="hljs-string">for</span> <span class="hljs-string">last</span> <span class="hljs-string">commit</span> <span class="hljs-string">version</span>
  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">and(eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">),</span> <span class="hljs-string">eq(variables['CI_CREATE_GIT_TAG'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">))</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<h4 id="heading-step-16-copy-package-files">Step 16: Copy package file(s)</h4>
<p>Now the <code>.sppkg</code> files are copied into the temporary <code>drop</code> folder.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">CopyFiles@2</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Copy Files to: $(Build.ArtifactStagingDirectory)/drop'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">Contents:</span> <span class="hljs-string">'**/*.sppkg'</span>
    <span class="hljs-attr">TargetFolder:</span> <span class="hljs-string">'$(Build.ArtifactStagingDirectory)/drop'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<h4 id="heading-step-17-publish-artifacts">Step 17: Publish Artifacts</h4>
<p>And finally, the artifacts have to be published so that the <code>.sppkg</code> files can be downloaded.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">PublishBuildArtifacts@1</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Publish Artifact: drop'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>That was it (almost)</p>
<h3 id="heading-the-complete-file">The complete file</h3>
<p>Here is the complete file:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># SPFx Build Pipeline</span>
<span class="hljs-comment"># Author: seryoga@spfx-app.dev</span>
<span class="hljs-comment"># Version: 1.0</span>
<span class="hljs-comment"># Steps:</span>
<span class="hljs-comment">#   Step 1: checkout the branch</span>
<span class="hljs-comment">#   Step 2: Check if gitconfig exists</span>
<span class="hljs-comment">#   Step 3: Set git user</span>
<span class="hljs-comment">#   Step 4: Check commit message and determine version bump option</span>
<span class="hljs-comment">#   Step 5: Use Node Version 16.x</span>
<span class="hljs-comment">#   Step 6: OPTIONAL (Disbaled): install pnpm (for pnpm projects only)</span>
<span class="hljs-comment">#   Step 7: Execute "(p)npm Install"-Command</span>
<span class="hljs-comment">#   Step 8: Execute "gulp clean"-Command</span>
<span class="hljs-comment">#   Step 9: (Disabled): Execute "gulp build --ship"-Command (not mandatory, you can disable this command when you want by adding to the task `enabled: false` after the `input`-section) </span>
<span class="hljs-comment">#   Step 10: Execute "gulp bump-version"-Command</span>
<span class="hljs-comment">#   Step 11: Execute "gulp bundle --ship --no-patch"-Command</span>
<span class="hljs-comment">#   Step 12: Execute "gulp package-solution --ship"-Command</span>
<span class="hljs-comment">#   Step 13: Read (new) version from package.json</span>
<span class="hljs-comment">#   Step 14: Push the changes back to repository</span>
<span class="hljs-comment">#   Step 15: Create Git Tag with the new version</span>
<span class="hljs-comment">#   Step 16: Copy Files to: $(Build.ArtifactStagingDirectory)/drop</span>
<span class="hljs-comment">#   Step 17: Publish Artifacts</span>
<span class="hljs-attr">trigger:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">development</span>
<span class="hljs-attr">pool:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">Azure</span> <span class="hljs-string">Pipelines</span>
  <span class="hljs-attr">vmImage:</span> <span class="hljs-string">'ubuntu-latest'</span>
<span class="hljs-attr">steps:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">checkout:</span> <span class="hljs-string">self</span>
  <span class="hljs-attr">clean:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">persistCredentials:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">powershell:</span> <span class="hljs-string">|
   $exists = Test-Path -Path $HOME/.gitconfig
   WRITE-HOST ".gitconfig exist:" $exists
</span>  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Check</span> <span class="hljs-string">whether</span> <span class="hljs-string">gitconfig</span> <span class="hljs-string">exists</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
   git config --global user.email devops@spfx-app.dev &amp; git config --global user.name "spfx-app.dev"
</span>  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">powershell:</span> <span class="hljs-string">|
   $commitMsg = "$(Build.SourceVersionMessage)"
   Write-Host $commitMsg
   Write-Host "Version bump is ENABLED"
</span>
   <span class="hljs-string">$bumpVersionArgs</span> <span class="hljs-string">=</span> <span class="hljs-string">"--no-patch"</span>
   <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_MAJOR_VERSION;]$false"</span>
   <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_MINOR_VERSION;]$false"</span>
   <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_PATCH_VERSION;]$false"</span>
   <span class="hljs-string">if($commitMsg</span> <span class="hljs-string">-match</span> <span class="hljs-string">"(major):.*"</span><span class="hljs-string">)</span> {
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"Bump Major Version"</span>
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_MAJOR_VERSION;]$true"</span>
      <span class="hljs-string">$bumpVersionArgs</span> <span class="hljs-string">=</span> <span class="hljs-string">"--major"</span>
    }
    <span class="hljs-string">elseif($commitMsg</span> <span class="hljs-string">-match</span> <span class="hljs-string">"(minor):.*"</span><span class="hljs-string">)</span> {
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"Bump Minor Version"</span>
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_MINOR_VERSION;]$true"</span>
      <span class="hljs-string">$bumpVersionArgs</span> <span class="hljs-string">=</span> <span class="hljs-string">"--minor"</span>
    }
    <span class="hljs-string">else</span> {
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"Bump Patch Version"</span>
      <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_PATCH_VERSION;]$true"</span>
      <span class="hljs-string">$bumpVersionArgs</span> <span class="hljs-string">=</span> <span class="hljs-string">"--patch"</span>
    }

    <span class="hljs-string">Write-Host</span> <span class="hljs-string">"##vso[task.setvariable variable=BUMP_VERSION_ARGS;]$bumpVersionArgs"</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Get</span> <span class="hljs-string">the</span> <span class="hljs-string">commit</span> <span class="hljs-string">message</span>
  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">)</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">NodeTool@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Use Node 16.x'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">versionSpec:</span> <span class="hljs-number">16.</span><span class="hljs-string">x</span>
    <span class="hljs-attr">checkLatest:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">Npm@1</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'npm install -g pnpm'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">workingDir:</span> <span class="hljs-string">' $(Build.Repository.LocalPath)'</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">'custom'</span>
    <span class="hljs-attr">customCommand:</span> <span class="hljs-string">'install -g pnpm'</span>
    <span class="hljs-attr">verbose:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">false</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">CmdLine@2</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'npm install'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">script:</span> <span class="hljs-string">'npm i'</span>
    <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">' $(Build.Repository.LocalPath)'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp clean'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">clean</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp build --ship'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">build</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">'--ship'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">false</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp bump-version'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">'bump-version'</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">$(BUMP_VERSION_ARGS)</span>
  <span class="hljs-attr">continueOnError:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">)</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp bundle --ship --no-patch'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">bundle</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">'--ship --no-patch'</span>
  <span class="hljs-attr">continueOnError:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">gulp@0</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'gulp package-solution --ship'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">targets:</span> <span class="hljs-string">'package-solution'</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">'--ship'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">powershell:</span> <span class="hljs-string">|
   $newVersion = (Get-content ./package.json| out-string | ConvertFrom-Json).version
   Write-Host "##vso[task.setvariable variable=NEW_VERSION;]$newVersion"
   Write-Host $newVersion
</span>  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Get</span> <span class="hljs-string">Version</span> <span class="hljs-string">from</span> <span class="hljs-string">package.json</span> <span class="hljs-string">and</span> <span class="hljs-string">set</span> <span class="hljs-string">in</span> <span class="hljs-string">variable</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">)</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
   echo $(NEW_VERSION)
   git add .
   git commit -m "[skip ci] Version updated to $(NEW_VERSION)"
   git push origin HEAD:$(Build.SourceBranch)
</span>  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Push</span> <span class="hljs-string">changes</span> <span class="hljs-string">to</span> <span class="hljs-string">source</span> <span class="hljs-string">branch</span>
  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">)</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
   git tag $(NEW_VERSION) HEAD
   git push origin --tags
</span>  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Create</span> <span class="hljs-string">Tag</span> <span class="hljs-string">for</span> <span class="hljs-string">last</span> <span class="hljs-string">commit</span> <span class="hljs-string">version</span>
  <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">$(System.DefaultWorkingDirectory)</span>
  <span class="hljs-attr">condition:</span> <span class="hljs-string">and(eq(variables['CI_BUMP_VERSION'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">),</span> <span class="hljs-string">eq(variables['CI_CREATE_GIT_TAG'],</span> <span class="hljs-string">'TRUE'</span><span class="hljs-string">))</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">CopyFiles@2</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Copy Files to: $(Build.ArtifactStagingDirectory)/drop'</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">Contents:</span> <span class="hljs-string">'**/*.sppkg'</span>
    <span class="hljs-attr">TargetFolder:</span> <span class="hljs-string">'$(Build.ArtifactStagingDirectory)/drop'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">PublishBuildArtifacts@1</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Publish Artifact: drop'</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<h4 id="heading-creating-the-variables">Creating the variables</h4>
<p>Before you save the pipeline, you must create variables. To do this, click on the "Variables" button at the top right of the editor and then on "+". Enter the variable name <code>CI_BUMP_VERSION</code> and the value <code>TRUE</code>. If you want to change these values when running manually, you have to select "Let users override this value when running this pipeline". Save the variable and do the same with the variable <code>CI_CREATE_GIT_TAG</code>. Save the settings and then save the pipeline. Now it is ready to run.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655751130129/ZlEshA19g.png" alt="Add new Variables" /></p>
<h2 id="heading-result">Result</h2>
<p>When the pipeline has gone through, it now looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655751167457/_lKwuIQT3.png" alt="Commit message" /></p>
<p>And the tags were created:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655751194163/rM9UXozoQ.png" alt="Git tags" /></p>
<p>Again, as a hint: Through the commit message you can define whether major, minor or patch version should be counted up.</p>
<p>Happy Coding ;-)</p>
]]></content:encoded></item><item><title><![CDATA[Package SPFx solution with one command and automatically increase the version]]></title><description><![CDATA[In SPFx solutions, the version number is stored in the package-solution.json file. The versioning follows the scheme used in assemblies, for example. For example, 1.0.0.0 (Major.Minor.Build.Revision).
Most people (and I have to admit that I am one of...]]></description><link>https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version</link><guid isPermaLink="true">https://spfx-app.dev/package-spfx-solution-with-one-command-and-automatically-increase-the-version</guid><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Fri, 17 Jun 2022 10:15:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1655460878003/J408J9w2y.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In SPFx solutions, the version number is stored in the package-solution.json file. The versioning follows the scheme used in assemblies, for example. For example, 1.0.0.0 (Major.Minor.Build.Revision).</p>
<p>Most people (and I have to admit that I am one of them) almost never incremented this number. At least the packages did not have to be "updated" manually and received all changes immediately. You also have to remember to update the version number. It is, of course, clear that this is not the purpose of versioning. But recently, Microsoft has published an important update, which obliges to update the version number. Because if you do not updates the version, the solution changes will not be visible. So you have two options: </p>
<ol>
<li><p>Uninstall solution, delete solution from the Recycle Bin, delete solution from the 2nd Stage Recycle Bin and then reinstall.</p>
</li>
<li><p>Update the version number and then perform an "update" for the app. </p>
</li>
</ol>
<p>I think it is clear which is faster/easier. But the problem is that you must not forget to update the version. I came across the "automatic version increment" on a blog a few years ago, which uses a <code>gulp</code> task. But - as described above - I didn't see the point in incrementing the version because the other thing is easier. Upload solution - done. No update, no waiting. It is simple and fast. With the Microsoft Update, such a task makes sense, of course. So I went looking for this script again and came across another - but still useful - <a target="_blank" href="https://thomasdaly.net/2018/08/21/update-spfx-automatically-generating-revision-numbers-versioning/">blog article by Tom Daly</a>. I found his code quite good and wanted to adapt it to my needs. </p>
<p>For my SPFx solutions that I publish on <a target="_blank" href="https://github.com/SPFxAppDev?tab=repositories">GitHub</a>, I include the version number in the web parts. This has become established for many WebParts. For this purpose, I usually use the PnP Property Pane Controls and the control <a target="_blank" href="https://pnp.github.io/sp-dev-fx-property-controls/controls/PropertyPaneWebPartInformation/">PropertyPaneWebPartInformation</a>. To display the version number, you can access the Web Part context object <code>this.context.manifest.version</code>. The problem is that this is not the version number from package-solutions.json, but the one from package.json. This is needed for <code>npm</code>. But <code>npm</code>, like many others, uses the <a target="_blank" href="https://semver.org/">Semantic Versioning</a>. This version has 3 digits. So 1.0.0 (Major.Minor.Patch). </p>
<p>My goal was to automatically update both, the <code>package-solution.json</code> and the <code>package.json</code> and to keep them "in sync". The revision number of the <code>package-solution.json</code> is completely ignored. The version number should always be incremented automatically when the <code>gulp bundle</code> command is executed with the parameter <code>--ship</code>. Alternatively, you can just update the version with the command <code>gulp bump-version</code>. You can specify which part of the versioning should be updated with the additional parameters <code>--major</code>, <code>--minor</code> or <code>--patch</code>. Whereas <code>--patch</code> is the default. If you don't want to update the version in the <code>gulp bundle --ship</code> command, you can just type <code>gulp bundle --ship --no-patch</code> and then nothing will be done with the version.</p>
<p>To use my script, you need to add the following lines to the top of <code>gulpfile.js</code> (if not already there):</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> gulp = <span class="hljs-built_in">require</span>(<span class="hljs-string">'gulp'</span>); 
<span class="hljs-keyword">const</span> gutil = <span class="hljs-built_in">require</span>(<span class="hljs-string">'gulp-util'</span>); 
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);
</code></pre>
<p>Then replace the last line <code>build.initialize(require('gulp'));</code> with <code>build.initialize(gulp);</code>. And before this line, insert the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> getJson = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">file</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(fs.readFileSync(file, <span class="hljs-string">'utf8'</span>));
};

<span class="hljs-keyword">let</span> bumpVersionSubTask = build.subTask(<span class="hljs-string">'bump-version-subtask'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">gulp, buildOptions, done</span>) </span>{

  <span class="hljs-keyword">const</span> currentCommand = buildOptions.args._[<span class="hljs-number">0</span>];

  <span class="hljs-keyword">const</span> skipFunc = gulp.src(<span class="hljs-string">'./config/package-solution.json'</span>).pipe(gutil.noop());

  <span class="hljs-keyword">if</span>(<span class="hljs-keyword">typeof</span> currentCommand != <span class="hljs-string">"string"</span>) {
    gutil.log(<span class="hljs-string">"The current command is undefined, skip version bump"</span>);
    <span class="hljs-keyword">return</span> skipFunc;
  }

  <span class="hljs-keyword">const</span> commandName = currentCommand.toLocaleLowerCase();

  <span class="hljs-keyword">if</span>(commandName != <span class="hljs-string">"bundle"</span> &amp;&amp; commandName != <span class="hljs-string">"bump-version"</span>) {
    gutil.log(<span class="hljs-string">"The current command is not 'bundle' or 'bump-version', skip version bump"</span>);
    <span class="hljs-keyword">return</span> skipFunc;
  }

  <span class="hljs-keyword">const</span> bumpVersion = commandName == <span class="hljs-string">"bump-version"</span> || buildOptions.args[<span class="hljs-string">"ship"</span>] === <span class="hljs-literal">true</span>;

  <span class="hljs-keyword">if</span>(!bumpVersion) {
      gutil.log(<span class="hljs-string">"The current command is not 'bump-version' or the --ship argument was not specified, skip version bump"</span>);
      <span class="hljs-keyword">return</span> skipFunc;
  }

  <span class="hljs-keyword">const</span> a = buildOptions.args;

  <span class="hljs-keyword">const</span> skipMajorVersion = <span class="hljs-keyword">typeof</span> a[<span class="hljs-string">"major"</span>] == <span class="hljs-string">"undefined"</span> || a[<span class="hljs-string">"major"</span>] === <span class="hljs-literal">false</span>;
  <span class="hljs-keyword">const</span> skipMinorVersion = !skipMajorVersion || <span class="hljs-keyword">typeof</span> a[<span class="hljs-string">"minor"</span>] == <span class="hljs-string">"undefined"</span> || a[<span class="hljs-string">"minor"</span>] === <span class="hljs-literal">false</span>;
  <span class="hljs-keyword">const</span> skipPatchVersion = !skipMajorVersion || !skipMinorVersion || a[<span class="hljs-string">"patch"</span>] === <span class="hljs-literal">false</span>;

  <span class="hljs-keyword">if</span>(skipMajorVersion &amp;&amp; skipMinorVersion &amp;&amp; skipPatchVersion) {
    gutil.log(<span class="hljs-string">"skip version bump, because all specified arguments (major, minor, patch) are set to 'false'"</span>)
    <span class="hljs-keyword">return</span> skipFunc;
  }

  <span class="hljs-keyword">const</span> pkgSolutionJson = getJson(<span class="hljs-string">'./config/package-solution.json'</span>);
  <span class="hljs-keyword">const</span> currentVersionNumber = <span class="hljs-built_in">String</span>(pkgSolutionJson.solution.version);
  <span class="hljs-keyword">let</span> nextVersionNumber = currentVersionNumber.slice();
  <span class="hljs-keyword">let</span> nextVersionSplitted = nextVersionNumber.split(<span class="hljs-string">'.'</span>);
  gutil.log(<span class="hljs-string">'Current version: '</span> + currentVersionNumber);

  <span class="hljs-keyword">if</span>(!skipMajorVersion) {
    nextVersionSplitted[<span class="hljs-number">0</span>] = <span class="hljs-built_in">parseInt</span>(nextVersionSplitted[<span class="hljs-number">0</span>]) + <span class="hljs-number">1</span>;
    nextVersionSplitted[<span class="hljs-number">1</span>] = <span class="hljs-number">0</span>;
    nextVersionSplitted[<span class="hljs-number">2</span>] = <span class="hljs-number">0</span>;
    nextVersionSplitted[<span class="hljs-number">3</span>] = <span class="hljs-number">0</span>;
  }

  <span class="hljs-keyword">if</span>(!skipMinorVersion) {
    nextVersionSplitted[<span class="hljs-number">1</span>] = <span class="hljs-built_in">parseInt</span>(nextVersionSplitted[<span class="hljs-number">1</span>]) + <span class="hljs-number">1</span>;
    nextVersionSplitted[<span class="hljs-number">2</span>] = <span class="hljs-number">0</span>;
    nextVersionSplitted[<span class="hljs-number">3</span>] = <span class="hljs-number">0</span>;
  }

  <span class="hljs-keyword">if</span>(!skipPatchVersion) {
    nextVersionSplitted[<span class="hljs-number">2</span>] = <span class="hljs-built_in">parseInt</span>(nextVersionSplitted[<span class="hljs-number">2</span>]) + <span class="hljs-number">1</span>;
    nextVersionSplitted[<span class="hljs-number">3</span>] = <span class="hljs-number">0</span>;
  }

  nextVersionNumber = nextVersionSplitted.join(<span class="hljs-string">"."</span>);

  gutil.log(<span class="hljs-string">'New version: '</span>, nextVersionNumber);

  pkgSolutionJson.solution.version = nextVersionNumber;
  fs.writeFile(<span class="hljs-string">'./config/package-solution.json'</span>, <span class="hljs-built_in">JSON</span>.stringify(pkgSolutionJson, <span class="hljs-literal">null</span>, <span class="hljs-number">4</span>), <span class="hljs-function">() =&gt;</span> {});

  <span class="hljs-keyword">const</span> packageJson = getJson(<span class="hljs-string">'./package.json'</span>);
  packageJson.version = nextVersionNumber.split(<span class="hljs-string">'.'</span>).splice(<span class="hljs-number">0</span>, <span class="hljs-number">3</span>).join(<span class="hljs-string">"."</span>);
  fs.writeFile(<span class="hljs-string">'./package.json'</span>, <span class="hljs-built_in">JSON</span>.stringify(packageJson, <span class="hljs-literal">null</span>, <span class="hljs-number">4</span>), <span class="hljs-function">() =&gt;</span> {});

  <span class="hljs-keyword">return</span> gulp.src(<span class="hljs-string">'./config/package-solution.json'</span>)
  .pipe(gulp.dest(<span class="hljs-string">'./config'</span>));
});

<span class="hljs-keyword">let</span> bumpVersionTask = build.task(<span class="hljs-string">'bump-version'</span>, bumpVersionSubTask);
build.rig.addPreBuildTask(bumpVersionTask);
</code></pre>
<p>The complete "gulpfile.js" looks like this:</p>
<pre><code class="lang-javascript"><span class="hljs-meta">'use strict'</span>;

<span class="hljs-keyword">const</span> gulp = <span class="hljs-built_in">require</span>(<span class="hljs-string">'gulp'</span>);
<span class="hljs-keyword">const</span> build = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@microsoft/sp-build-web'</span>);
<span class="hljs-keyword">const</span> gutil = <span class="hljs-built_in">require</span>(<span class="hljs-string">'gulp-util'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

build.addSuppression(<span class="hljs-string">`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`</span>);

<span class="hljs-keyword">var</span> getTasks = build.rig.getTasks;
build.rig.getTasks = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">var</span> result = getTasks.call(build.rig);

  result.set(<span class="hljs-string">'serve'</span>, result.get(<span class="hljs-string">'serve-deprecated'</span>));

  <span class="hljs-keyword">return</span> result;
};

<span class="hljs-keyword">var</span> getJson = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">file</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(fs.readFileSync(file, <span class="hljs-string">'utf8'</span>));
};

<span class="hljs-keyword">let</span> bumpVersionSubTask = build.subTask(<span class="hljs-string">'bump-version-subtask'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">gulp, buildOptions, done</span>) </span>{

  <span class="hljs-keyword">const</span> currentCommand = buildOptions.args._[<span class="hljs-number">0</span>];

  <span class="hljs-keyword">const</span> skipFunc = gulp.src(<span class="hljs-string">'./config/package-solution.json'</span>).pipe(gutil.noop());

  <span class="hljs-keyword">if</span>(<span class="hljs-keyword">typeof</span> currentCommand != <span class="hljs-string">"string"</span>) {
    gutil.log(<span class="hljs-string">"The current command is undefined, skip version bump"</span>);
    <span class="hljs-keyword">return</span> skipFunc;
  }

  <span class="hljs-keyword">const</span> commandName = currentCommand.toLocaleLowerCase();

  <span class="hljs-keyword">if</span>(commandName != <span class="hljs-string">"bundle"</span> &amp;&amp; commandName != <span class="hljs-string">"bump-version"</span>) {
    gutil.log(<span class="hljs-string">"The current command is not 'bundle' or 'bump-version', skip version bump"</span>);
    <span class="hljs-keyword">return</span> skipFunc;
  }

  <span class="hljs-keyword">const</span> bumpVersion = commandName == <span class="hljs-string">"bump-version"</span> || buildOptions.args[<span class="hljs-string">"ship"</span>] === <span class="hljs-literal">true</span>;

  <span class="hljs-keyword">if</span>(!bumpVersion) {
      gutil.log(<span class="hljs-string">"The current command is not 'bump-version' or the --ship argument was not specified, skip version bump"</span>);
      <span class="hljs-keyword">return</span> skipFunc;
  }

  <span class="hljs-keyword">const</span> a = buildOptions.args;

  <span class="hljs-keyword">const</span> skipMajorVersion = <span class="hljs-keyword">typeof</span> a[<span class="hljs-string">"major"</span>] == <span class="hljs-string">"undefined"</span> || a[<span class="hljs-string">"major"</span>] === <span class="hljs-literal">false</span>;
  <span class="hljs-keyword">const</span> skipMinorVersion = !skipMajorVersion || <span class="hljs-keyword">typeof</span> a[<span class="hljs-string">"minor"</span>] == <span class="hljs-string">"undefined"</span> || a[<span class="hljs-string">"minor"</span>] === <span class="hljs-literal">false</span>;
  <span class="hljs-keyword">const</span> skipPatchVersion = !skipMajorVersion || !skipMinorVersion || a[<span class="hljs-string">"patch"</span>] === <span class="hljs-literal">false</span>;

  <span class="hljs-keyword">if</span>(skipMajorVersion &amp;&amp; skipMinorVersion &amp;&amp; skipPatchVersion) {
    gutil.log(<span class="hljs-string">"skip version bump, because all specified arguments (major, minor, patch) are set to 'false'"</span>)
    <span class="hljs-keyword">return</span> skipFunc;
  }

  <span class="hljs-keyword">const</span> pkgSolutionJson = getJson(<span class="hljs-string">'./config/package-solution.json'</span>);
  <span class="hljs-keyword">const</span> currentVersionNumber = <span class="hljs-built_in">String</span>(pkgSolutionJson.solution.version);
  <span class="hljs-keyword">let</span> nextVersionNumber = currentVersionNumber.slice();
  <span class="hljs-keyword">let</span> nextVersionSplitted = nextVersionNumber.split(<span class="hljs-string">'.'</span>);
  gutil.log(<span class="hljs-string">'Current version: '</span> + currentVersionNumber);

 <span class="hljs-keyword">if</span>(!skipMajorVersion) {
    nextVersionSplitted[<span class="hljs-number">0</span>] = <span class="hljs-built_in">parseInt</span>(nextVersionSplitted[<span class="hljs-number">0</span>]) + <span class="hljs-number">1</span>;
    nextVersionSplitted[<span class="hljs-number">1</span>] = <span class="hljs-number">0</span>;
    nextVersionSplitted[<span class="hljs-number">2</span>] = <span class="hljs-number">0</span>;
    nextVersionSplitted[<span class="hljs-number">3</span>] = <span class="hljs-number">0</span>;
  }

  <span class="hljs-keyword">if</span>(!skipMinorVersion) {
    nextVersionSplitted[<span class="hljs-number">1</span>] = <span class="hljs-built_in">parseInt</span>(nextVersionSplitted[<span class="hljs-number">1</span>]) + <span class="hljs-number">1</span>;
    nextVersionSplitted[<span class="hljs-number">2</span>] = <span class="hljs-number">0</span>;
    nextVersionSplitted[<span class="hljs-number">3</span>] = <span class="hljs-number">0</span>;
  }

  <span class="hljs-keyword">if</span>(!skipPatchVersion) {
    nextVersionSplitted[<span class="hljs-number">2</span>] = <span class="hljs-built_in">parseInt</span>(nextVersionSplitted[<span class="hljs-number">2</span>]) + <span class="hljs-number">1</span>;
    nextVersionSplitted[<span class="hljs-number">3</span>] = <span class="hljs-number">0</span>;
  }

  nextVersionNumber = nextVersionSplitted.join(<span class="hljs-string">"."</span>);

  gutil.log(<span class="hljs-string">'New version: '</span>, nextVersionNumber);

  pkgSolutionJson.solution.version = nextVersionNumber;
  fs.writeFile(<span class="hljs-string">'./config/package-solution.json'</span>, <span class="hljs-built_in">JSON</span>.stringify(pkgSolutionJson, <span class="hljs-literal">null</span>, <span class="hljs-number">4</span>), <span class="hljs-function">() =&gt;</span> {});

  <span class="hljs-keyword">const</span> packageJson = getJson(<span class="hljs-string">'./package.json'</span>);
  packageJson.version = nextVersionNumber.split(<span class="hljs-string">'.'</span>).splice(<span class="hljs-number">0</span>, <span class="hljs-number">3</span>).join(<span class="hljs-string">"."</span>);
  fs.writeFile(<span class="hljs-string">'./package.json'</span>, <span class="hljs-built_in">JSON</span>.stringify(packageJson, <span class="hljs-literal">null</span>, <span class="hljs-number">4</span>), <span class="hljs-function">() =&gt;</span> {});

  <span class="hljs-keyword">return</span> gulp.src(<span class="hljs-string">'./config/package-solution.json'</span>)
  .pipe(gulp.dest(<span class="hljs-string">'./config'</span>));
});

<span class="hljs-keyword">let</span> bumpVersionTask = build.task(<span class="hljs-string">'bump-version'</span>, bumpVersionSubTask);
build.rig.addPreBuildTask(bumpVersionTask);

build.initialize(gulp);
</code></pre>
<p>Now you can use the task <code>gulp bundle --ship</code> or <code>gulp bump-version</code> to automatically bump up the version.</p>
<h2 id="heading-one-command-for-complete-release-including-version-update">One command for complete release including version update</h2>
<p>To publish the package, you normally have to execute the following commands:</p>
<pre><code class="lang-bash">gulp clean

gulp build

gulp bundle --ship

gulp package-solution --ship
</code></pre>
<p>Of course, you can also use the shorthand notation <code>gulp clean; gulp build; gulp bundle --ship; gulp package-solution --ship</code>. But this also takes a long time. That's why I show you here how you can do everything at once with one command. Including version updates. I have thought of the command <code>publish</code> for this. You can simply use the <code>npm scripts</code> for this. Open the <code>package.json</code> file and enter the following under the <code>scripts</code> property:</p>
<pre><code class="lang-json"><span class="hljs-string">"publish"</span>: <span class="hljs-string">"gulp clean &amp;&amp; gulp build &amp;&amp; gulp bundle --ship"</span>,
<span class="hljs-string">"postpublish"</span>: <span class="hljs-string">"gulp package-solution --ship"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663602011755/tdeIjvldK.png" alt="package.json file example" /></p>
<p>Now you can just run <code>npm run publish</code> in the terminal and you have everything in one command. Even the version is incremented because of our previously described <code>gulp</code> task.</p>
<p>By the way, if you don't want the version to be incremented, just run the command like this <code>npm run publish -- --no-patch</code>. If the major version is to be updated, then just use <code>npm run publish -- --major</code>. For minor then <code>npm run publish -- --minor</code>.</p>
<p>I hope it has helped you and that you - like me - have saved some time.</p>
<p>Happy coding ;-)</p>
]]></content:encoded></item><item><title><![CDATA[A simple password vault webpart for Microsoft SharePoint/Teams]]></title><description><![CDATA[Some time ago my colleague, who is our internal security officer, asked me how much effort it would take to develop a SharePoint web part to encrypt certain data so that it is not in plain text. I replied that a "simple webpart" can be implemented ve...]]></description><link>https://spfx-app.dev/a-simple-password-vault-webpart-for-microsoft-sharepointteams</link><guid isPermaLink="true">https://spfx-app.dev/a-simple-password-vault-webpart-for-microsoft-sharepointteams</guid><category><![CDATA[crypto]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Fri, 20 May 2022 10:10:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1653041375928/6Oy_uZ3PE.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Some time ago my colleague, who is our internal security officer, asked me how much effort it would take to develop a SharePoint web part to encrypt certain data so that it is not in plain text. I replied that a "simple webpart" can be implemented very quickly, with little effort. He had only asked me this out of interest, but nevertheless, the idea of a simple password vault webpart was born (of course, I asked my colleague if I could develop a webpart and then publish it).</p>
<p>I have seen sensitive data such as user name and password in plain text in many intranets. Why should one worry about such things on an intranet? After all, it can only be seen by the staff, can't it?</p>
<p>But something like that is still not secure if it is in plain text. Let's say I'm in a team meeting (which is even being recorded) and I am sharing my screen and I visit the page that contains the login data. And the data could be "stolen". This is just one example of many.</p>
<p>I then had the idea of developing a webpart where you can set a master password to encrypt the user name and password. And only after entering this master password, the entered data should be decrypted and displayed in plain text. A typical password vault such as Dashlane, KeyPass or Bitwarden. Only somewhat simplified and only for one user name and password. To be able to enter further information/notes, I have added a text field (Rich text editor), which is also encrypted. This could be used, for example, to store several user data.</p>
<p>The webpart should contain the following features:</p>
<ul>
<li><p>If you have not yet set a master password (webpart newly added). The vault does not have to be unlocked. The master password has to be set first.</p>
</li>
<li><p>If a master password has already been set, the vault must first be unlocked to view or adjust the data.</p>
</li>
<li><p>The data must also be stored encrypted in SharePoint. This means that the data must not be visible in SharePoint maintenance mode.</p>
</li>
<li><p>The encrypted values can only be decrypted with the master password.</p>
</li>
<li><p>The user name and password can be copied to the clipboard with one click.</p>
</li>
<li><p>The password is in a password field and is therefore not displayed unless it is actively displayed by the user.</p>
</li>
<li><p>The vault can be closed manually or will be closed automatically after 5 minutes after the page has been refreshed.</p>
</li>
</ul>
<p>Now it was time for the implementation. For encryption and decryption, I decided to use the JavaScript library <a target="_blank" href="https://cryptojs.gitbook.io/docs/">crypto-js</a>, which I already know and which is loved by many. The master password is hashed (and salted) with SHA256. The other data is logically not hashed, but encrypted and decrypted with the Advanced Encryption Standard (AES). A combination of a key defined by me + master password is used as the key value for encryption and decryption.</p>
<h2 id="heading-result">Result</h2>
<h3 id="heading-display-mode">Display Mode</h3>
<p>If the vault is closed, the user has to enter the master password (does not matter if in display or edit mode)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653040113555/E0aCPofcr.png" alt="A closed vault" /></p>
<p>After the user has entered the correct master password, he/she can view the stored data. The password is not visible in plain text unless the "show password" icon has been clicked on</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653040317122/65LwSsJpy.png" alt="The vault is open" /></p>
<h3 id="heading-edit-mode">Edit Mode</h3>
<p>In page edit mode, it is possible to update the master password or change the data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653040394650/dX-r4wl8I.png" alt="Edit mode" /></p>
<p>And yes, if you lose the master password, all data cannot be recovered. Not even by the administrator.</p>
<h3 id="heading-maintenance-mode">Maintenance Mode</h3>
<p>And to prove that the data is encrypted and is not displayed in maintenance mode, here is an example of the data stored in the webpart.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1653040608119/-fZVJ0oeH.png" alt="Maintenance Mode" /></p>
<h2 id="heading-source-and-download">Source and Download</h2>
<p>You can find the source code in my <a target="_blank" href="https://github.com/SPFxAppDev/sp-passwordvault-webpart">GitHub repository</a>. If you want to test/use the web part you can find the <a target="_blank" href="https://github.com/SPFxAppDev/sp-passwordvault-webpart/releases">SharePoint package file here</a>.</p>
<p>I would appreciate your feedback. You can leave a comment here in the post or use my <a target="_blank" href="https://github.com/SPFxAppDev/sp-passwordvault-webpart/issues">GitHub repository</a> for it.</p>
<h3 id="heading-update-march-13-2023">Update March 13, 2023</h3>
<p>I have updated the solution and added new features. <a target="_blank" href="https://spfx-app.dev/new-version-of-password-vault-webpart-was-released">See here</a></p>
]]></content:encoded></item><item><title><![CDATA[My interactive maps app for Microsoft Teams/SharePoint]]></title><description><![CDATA[Have you ever wished you had a module in SharePoint/Microsoft Teams to set map markers? You will probably say, that's already in SharePoint, the "Bing Maps" web part. That's right, but with this web part, you can set exactly one marker, without many ...]]></description><link>https://spfx-app.dev/my-interactive-maps-app-for-microsoft-teamssharepoint</link><guid isPermaLink="true">https://spfx-app.dev/my-interactive-maps-app-for-microsoft-teamssharepoint</guid><category><![CDATA[MicrosoftTeams]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Mon, 07 Mar 2022 12:56:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1646649324444/uQRKetwxN.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever wished you had a module in SharePoint/Microsoft Teams to set map markers? You will probably say, that's already in SharePoint, the "Bing Maps" web part. That's right, but with this web part, you can set exactly one marker, without many configuration options. You can't configure whether to allow "zooming" or change the map type (satellite, street map, bird's eye view, etc.) and certainly can't add multiple markers or customize the appearance of the Markers. That is what the Bing Maps web part looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1646647186140/BJFcFdYkR.png" alt="Standard SharePoint Bing Map Webpart" /></p>
<p>The only possibility is to define a label and whether it should be displayed or not. And, of course, the "position" (address) itself. </p>
<p>In my off time, after general working hours and on weekends, I took the time to provide a SPFx solution that has/can do everything I think you need. It took me about 24h of development time spread over many days. 
With my web part, you can place as many markers as you want. You can set if a tooltip is shown when hovering the mouse over the marker and also what kind of text should be shown in this tooltip. You can adjust the pin color and the icon displayed in it or omit it completely. You can also define what should happen when you click on the marker. Should a panel with further information open or maybe a dialog with freely definable text? Maybe nothing should happen or you should be redirected to a page (URL). There would also be the possibility to display the entered URL embedded in a dialog (iFrame). If there are many markers, you can decide if they should be clustered. Then you can see circles with the number of markers when you zoom out further. You don't like the tile layer (map "look and feel")? No problem, then change that too. Do you want to define categories, so that you don't have to "design" every marker anew and to show a legend for it? This is also no problem. It is also easier to edit categories afterwards. This is because all markers that have been assigned to a category are also updated after editing. It goes so far that you can even make the map static. This means that the user can neither zoom nor switch to another (map) point (dragging disabled). The web part can also be integrated into Microsoft Teams as a tab. Here you can see an example of my web part:</p>
<p><img src="https://spfxappdev.github.io/sp-map-webpart/images/MapWPOverview.gif" alt="My interactive maps webpart" /></p>
<p>There are many use cases for my solution:</p>
<ul>
<li>Display all locations of the company</li>
<li>Marking all customers on a map</li>
<li>Plan the next team event and show the meeting points, including the information about it</li>
<li>A static page header as a "banner" for the location page</li>
<li>etc.</li>
</ul>
<p>Curious? <a target="_blank" href="https://spfxappdev.github.io/sp-map-webpart/">Then visit the product website and learn how to download, install and use it.</a></p>
<p>I would appreciate your feedback. You can leave a comment here in the post or use my <a target="_blank" href="https://github.com/SPFxAppDev/sp-map-webpart/issues">GitHub repository</a> for it.</p>
]]></content:encoded></item><item><title><![CDATA[How Docker containers eliminate SPFx environment setup]]></title><description><![CDATA[The SharePoint Framework (SPFx) is a package that is updated very often. On the one hand, this is very good, but on the other hand, it is also a problem. Because, depending on the version, it uses different node, npm, TypeScript, and React versions (...]]></description><link>https://spfx-app.dev/how-docker-containers-eliminate-spfx-environment-setup</link><guid isPermaLink="true">https://spfx-app.dev/how-docker-containers-eliminate-spfx-environment-setup</guid><category><![CDATA[Docker]]></category><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[$€®¥09@]]></dc:creator><pubDate>Fri, 18 Feb 2022 15:59:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1645264316490/r7AuFQCbR.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The SharePoint Framework (SPFx) is a package that is updated very often. On the one hand, this is very good, but on the other hand, it is also a problem. Because, depending on the version, it uses different node, npm, TypeScript, and React versions (and of course other packages). The first SPFx version (1.0.0), released almost exactly 5 years ago, requires Node v6 LTS and npm v3.</p>
<p>The SPFx version 1.4.1 (for SP 2019 on-premises) already supported Node v8 LTS and also npm v4.</p>
<p>Yesterday (February 17, 2022) SPFx was released in version 1.14. This Version requires at least Node v12 LTS. You can find a listing of compatible versions <a target="_blank" href="https://docs.microsoft.com/en-us/sharepoint/dev/spfx/compatibility">here</a>.</p>
<p>That's no problem, you might think. Then the latest version is installed or updated to the latest version. Well, it's not quite that simple. What if you still have "old" projects. Projects that you have to continue to maintain and that were developed for SP2019 or even SP2016, for example. Because installing two different Node versions at the same time is not possible on one computer. Sure, you can use a VM. But then you have many VMs that also consume CPU/RAM and especially disk space unnecessarily. And you have to set up each of them.</p>
<h2 id="heading-setup-the-docker-container">Setup the docker Container</h2>
<p>My next project (not SharePoint) is supposed to use Docker. Docker is of course a term I know, after all, everyone talks about it. But I had no experience with it. To not start with this project without knowledge about Docker, I watched a <a target="_blank" href="https://www.youtube.com/watch?v=DESdVoKhIxY">super good video on Youtube (in German)</a> by <a target="_blank" href="https://github.com/goloroden">Golo Roden</a> two days ago (BTW: You can find the same article in German <a target="_blank" href="https://spfx-app.dev/wie-docker-container-die-einrichtung-der-spfx-umgebung-uberflussig-machen">here</a>). He explained it insanely well and I now probably have more than basic knowledge about Docker. After that I thought, actually, you could create something like that for SPFx. Then you wouldn't have to set up the environment at all (saves time) and you can use different versions at the same time. Before I create my own Dockerfile, I checked if there is already a Docker image for SPFx. There is <a target="_blank" href="https://hub.docker.com/r/m365pnp/spfx">one</a>. I wanted to try that out right away. By the way, I'm <strong>not</strong> going to go into how you set up/install Docker or what it is, how it works, or how to use it. That's not relevant to the article either. There is also <strong>no</strong> Docker knowledge required.</p>
<p>Two days ago SPFx 1.14 was not officially released. Therefore I downloaded the image for SPFx 1.13. The <a target="_blank" href="https://hub.docker.com/r/m365pnp/spfx">documentation for the Docker image</a> is quite good, however, I stumbled upon a couple of obstacles. Already in the first step!</p>
<p><code>in Docker Settings &gt; Shared Drives verify that the drive where you create your projects is shared</code></p>
<p>This setting does not exist for me! This has several reasons. The "Shared Drive" has not been available under <a target="_blank" href="https://www.docker.com/products/docker-desktop">Docker Desktop</a> for quite some time. Actually, it should be under <code>Docker Settings &gt; Resources &gt; File Sharing</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645195399028/XbfRLlRQS.png" alt="Where the Docker Settings should be" /></p>
<p>Yes, I know, I need to update my Docker Desktop 😜</p>
<p>But even this setting is not to be found in my case. It must be said that I have a Windows 10 operating system and use WSL 2 (Windows Subsystem for Linux).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645195487297/Q1bNka9My.png" alt="Docker Desktop with WSL 2" /></p>
<p>Under WSL 2, you don't need a file share because everything runs on the local machine instead of on a Hyper-V backend.</p>
<p>After I figured that out, I wanted to get started on the next steps. So I created a folder. Then entered the command there as described in the <a target="_blank" href="https://hub.docker.com/r/m365pnp/spfx">documentation</a>:</p>
<pre><code class="lang-bash">docker run -it --rm --name spfx-helloworld -v <span class="hljs-variable">${PWD}</span>:/usr/app/spfx -p 4321:4321 -p 35729:35729 m365pnp/spfx
</code></pre>
<p>Let me briefly explain the commands:</p>
<p><code>docker run</code> creates a new container</p>
<p><code>-it</code> stands for <code>interactive</code>, i.e. you stay "in the container" after creating it. This is mostly a terminal/bash window</p>
<p><code>--rm</code> means that after the container is terminated, it will be deleted as well.</p>
<p><code>--name</code> is the name of the container</p>
<p><code>-v</code> is the volume mapping. The current local path (<code>${PWD}</code>) is mapped to the container path usr/app/spfx.</p>
<p><code>-p</code> stands for port. This maps the local port <code>4321</code> to the container port <code>4321</code>. And the port <code>35729</code> with the container port <code>35729</code>.</p>
<p>And finally the name of the image is specified ==&gt; <a target="_blank" href="https://hub.docker.com/r/m365pnp/spfx">m365pnp/spfx</a>.</p>
<p>The container is created and a shell opens. This shell is the shell of the container. That's why you have to specify all your <code>yo</code>, <code>npm</code>, <code>gulp</code> and other commands there and <strong>not</strong> on the local computer.</p>
<p>That means, after the <code>yo @microsoft/sharepoint</code> command, type all your <code>npm</code>, <code>gulp</code> etc. commands. Only when you are 100% done (e.g. package published, testing finished etc.), enter <code>exit</code>. After that, the container is finished and will be deleted immediately because of the <code>--rm</code> parameter. But don't worry, not the project. It is on your local computer. If you want to work with the project later, you simply have to create the container again (in the project directory) as described above.</p>
<p>Theoretically, you could now serve the project with <code>gulp serve</code>. But this will not work the first time. This is also described <a target="_blank" href="https://hub.docker.com/r/m365pnp/spfx">in the Docker image documentation</a>. Because you first need to edit the <code>serve.json</code> file, which is located in the <code>config</code> folder. For this, you have to set the <code>hostname</code> to <code>0.0.0.0</code>.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://developer.microsoft.com/json-schemas/core-build/serve.schema.json"</span>,
  <span class="hljs-attr">"port"</span>: <span class="hljs-number">4321</span>,
  <span class="hljs-attr">"hostname"</span>: <span class="hljs-string">"0.0.0.0"</span>,
  <span class="hljs-attr">"https"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"initialPage"</span>: <span class="hljs-string">"https://yourtenant.sharepoint.com/_layouts/workbench.aspx"</span>
}
</code></pre>
<p>Because I used - as described at the beginning - the SPFx version 1.13, according to the instructions, I also had to edit a JavaScript file in line 393, which should be located here:</p>
<p><code>node_modules\@microsoft\spfx-heft-plugins\lib\plugins\webpackConfigurationPlugin\WebpackConfigurationGenerator.js</code></p>
<p>But this is not correct either. Because this file is located under:</p>
<p><code>node_modules\@microsoft\sp-build-web\node_modules\@microsoft\spfx-heft-plugins\lib\plugins\webpackConfigurationPlugin\WebpackConfigurationGenerator.js</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645196121048/0m1ZkZeeN.png" alt="The file to edit" /></p>
<p>You don't have to do this step anymore in the new SPFx version 1.14. Microsoft itself now checks whether you are in a container or not (<a target="_blank" href="https://docs.microsoft.com/en-us/sharepoint/dev/spfx/release-1.14#ipaddress-property-in-servejson">by ipAddress-Property in serve.json</a>). If you are using a different version, please check the <a target="_blank" href="link">Docker image documentation</a> to see if there is anything else you need to be aware of. By the way, version 1.14 also fixed my <a target="_blank" href="https://github.com/SharePoint/sp-dev-docs/issues/5787">reported bug</a> with the <a target="_blank" href="https://spfx-app.dev/sharepoint-spfx-why-you-shouldnt-use-fullmask-check-yet">Fullmask Permission Check</a> 😊</p>
<p>Now you could run <code>gulp serve</code> and call the SharePoint workbench. <code>https://{tenant}.sharepoint.com/_layouts/15/workbench.aspx</code></p>
<p>But you will find that your component is not present/listed. This issue is not listed at all in the <a target="_blank" href="https://hub.docker.com/r/m365pnp/spfx">Docker Image documentation</a>. The reason is that the certificate is not trusted. It doesn't matter if you already ran <code>gulp trust-dev-cert</code> on your local environment or if you did it in the container. To display the SPFx component you have to call the URL <code>https://localhost:4321</code> (on the local environment). After that, you have to trust the certificate.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645196549307/JL8DiyorD.png" alt="Trust certificate in browser" /></p>
<p>Your component should now be listed in the workbench (or wherever).</p>
<h2 id="heading-spfx-fast-serve-in-docker-container">SPFx Fast Serve in Docker Container</h2>
<p>I am a very big fan of the <a target="_blank" href="https://github.com/s-KaiNet/spfx-fast-serve">spfx-fast-serve Package</a> (if you don't know it, you must use it!). That's why I tried to use <code>spfx-fast-serve</code> in this container too. There was a <a target="_blank" href="https://github.com/s-KaiNet/spfx-fast-serve/issues/47">GitHub issue</a> related to Docker Container for this package, which was already solved, but the "workaround" was not acceptable to me like this. Therefore I tried to solve the problem in a different way. Since yesterday there is also a solution for this. <a target="_blank" href="https://github.com/s-KaiNet">Sergei Sergeev</a>, the author of the package, will now officially include <a target="_blank" href="https://github.com/s-KaiNet/spfx-fast-serve/issues/47#issuecomment-1042910356">my solution</a> in his readme.</p>
<p>To use <code>spfx-fast-serve</code> in your container, you just have to open <code>fast-serve/webpack.extend.js</code> and adjust the constant <code>webpackConfig</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> webpackConfig = {
  <span class="hljs-attr">devServer</span>: {
    <span class="hljs-attr">host</span>: <span class="hljs-string">'0.0.0.0'</span>,
    <span class="hljs-attr">publicPath</span>: <span class="hljs-string">'https://0.0.0.0:4321/dist/'</span>,
    <span class="hljs-attr">sockHost</span>: <span class="hljs-string">'localhost'</span>,
    <span class="hljs-attr">watchOptions</span>: {
      <span class="hljs-attr">aggregateTimeout</span>: <span class="hljs-number">500</span>, <span class="hljs-comment">// delay before reloading</span>
      <span class="hljs-attr">poll</span>: <span class="hljs-number">1000</span> <span class="hljs-comment">// enable polling since fsevents are not supported in docker</span>
    }
  },
  <span class="hljs-attr">output</span>: {
    <span class="hljs-attr">publicPath</span>: <span class="hljs-string">'https://0.0.0.0:4321/dist/'</span>
  }
}
</code></pre>
<p>Now the <code>npm run serve</code> command also works in the container.</p>
<h2 id="heading-custom-docker-image-70-smaller-and-with-spfx-fast-serve">Custom Docker image. 70% Smaller and with spfx-fast-serve</h2>
<p>The <a target="_blank" href="https://hub.docker.com/r/m365pnp/spfx">official Docker image</a> is already very good and helpful. What I didn't like was the image size. It is 1.22 GB and I didn't want to install the <code>spfx-fast-serve</code> package all the time. Therefore I thought about creating my own image. I learned in the <a target="_blank" href="https://www.youtube.com/watch?v=DESdVoKhIxY">Youtube Video</a> that the <a target="_blank" href="https://alpinelinux.org/">Linux Alpine</a> operating system is much smaller. Because it is delivered without certain packages, which the other Linux distributions use. This means that with the <a target="_blank" href="https://hub.docker.com/r/m365pnp/spfx">SPFx Docker Image</a>, the Linux distribution takes up so much space or better said, the used Node image uses this Linux version. But you can also use Node in the <a target="_blank" href="https://alpinelinux.org/">Alpine</a> version. So I copied the <a target="_blank" href="https://github.com/pnp/docker-spfx/blob/master/Dockerfile">original Dockerfile</a> and made it "Alpine"-Ready. Because the commands for Alpine are different from those of Ubuntu, Debian etc. I translated it, so to speak.</p>
<p>The result is the following Dockerfile with the default integration of the package <code>spfx-fast-serve</code>:</p>
<pre><code class="lang-bash">FROM node:14.19.0-alpine3.15

EXPOSE 5432 4321 35729

ENV NPM_CONFIG_PREFIX=/usr/app/.npm-global \
  PATH=<span class="hljs-variable">$PATH</span>:/usr/app/.npm-global/bin

VOLUME /usr/app/spfx
WORKDIR /usr/app/spfx

RUN apk add sudo &amp;&amp; \
    apk --no-cache add shadow &amp;&amp; \
    <span class="hljs-built_in">echo</span> <span class="hljs-string">'%wheel ALL=(ALL) ALL'</span> &gt; /etc/sudoers.d/wheel &amp;&amp; \
    adduser --home <span class="hljs-string">"<span class="hljs-subst">$(pwd)</span>"</span> --disabled-password --ingroup wheel --shell /bin/ash spfx &amp;&amp; \
    usermod -aG wheel spfx &amp;&amp; \
    chown -R spfx:wheel /usr/app

USER spfx

RUN npm i -g gulp@4 yo @microsoft/generator-sharepoint@1.13.1 spfx-fast-serve

CMD /bin/ash
</code></pre>
<p>If you compare the image sizes, <a target="_blank" href="https://hub.docker.com/r/seryoga/spfx">my image</a> is 70% smaller, and only 400MB.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645197080145/kaVplMflA.png" alt="Image sizes compared" /></p>
<p>In this container you have to do the same steps I described above. After creating the solution you can run <code>spfx-fast-serve</code> once (it doesn't need to be installed anymore, that was done in the Docker image) and then <code>npm i</code> and you can use <code>npm run serve</code>.</p>
<p>That's it. Happy Coding ;)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645197241212/GXv6Hiy2y.png" alt="SPFx in Docker Container" /></p>
]]></content:encoded></item></channel></rss>